From f6ec746b68a073cb4830384f94e9491088de95c8 Mon Sep 17 00:00:00 2001 From: IronTony Date: Sun, 13 Sep 2026 14:29:06 -0400 Subject: [PATCH 1/2] fix(plugin): bump adm-zip to 0.6.1 adm-zip 0.5.17 is the only runtime dependency and carries two advisories. GHSA-xcpc-8h2w-3j85 (high) lets an archive that declares a huge uncompressed size force an unbounded allocation. GHSA-vwc7-r8mq-g2x9 (moderate) lets extraction write through destination symlinks. Neither is reachable from the plugin. It never extracts to disk, and it only parses the AAR it downloaded after checking its SHA-256 against the pin, or its own cached copy under node_modules. Consumers' npm audit still traced both back to this package. 0.6 ships its own types, so @types/adm-zip goes. Its breaking changes (extractEntryTo path handling, Node 14 minimum) touch nothing the plugin calls. The strip was run against the real sdk-pdf-2.1.4.aar on 0.6.1: the result passes unzip -t and classes.jar no longer holds the Glide class. --- CHANGELOG.md | 6 ++++++ package-lock.json | 21 +++++---------------- package.json | 3 +-- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b237075..c110eb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Next + +### Fixes + +- Bump `adm-zip`, the Config Plugin's only runtime dependency, to 0.6.1. This clears GHSA-xcpc-8h2w-3j85 (high) and GHSA-vwc7-r8mq-g2x9 (moderate) from consumers' `npm audit`. Neither was reachable: the plugin never extracts to disk, and it only parses the AAR it downloaded after checking its SHA-256, or its own cached copy under `node_modules`. + ## 2.0.0 Upgrading from 1.x: every failure now rejects with a `DocuSignError`. Check any code that branches on `error.code`, matches message text, handles `status: 'error'` from `presentCaptiveSigning*`, or reads `errorCode` in an `addSigningErrorListener` callback. Codes are the lowercase codes the README documents, on both platforms, where iOS previously emitted `ERR_`-prefixed variants and Android rejected most failures as `signing_failed`. The new Android `launchStrategy` is opt-in. diff --git a/package-lock.json b/package-lock.json index 6f16327..609594f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,14 +9,13 @@ "version": "2.0.0", "license": "MIT", "dependencies": { - "adm-zip": "^0.5.17" + "adm-zip": "^0.6.1" }, "devDependencies": { "@amplitude/analytics-react-native": "^1.8.0", "@react-native/jest-preset": "^0.85.2", "@sentry/react-native": "^8.26.0", "@testing-library/react-native": "^13.3.3", - "@types/adm-zip": "^0.5.8", "@types/jest": "^29.5.14", "@types/node": "^20.19.39", "eslint": "^9.39.5", @@ -4952,16 +4951,6 @@ "node": ">= 10" } }, - "node_modules/@types/adm-zip": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.8.tgz", - "integrity": "sha512-RVVH7QvZYbN+ihqZ4kX/dMiowf6o+Jk1fNwiSdx0NahBJLU787zkULhGhJM8mf/obmLGmgdMM0bXsQTmyfbR7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -5482,12 +5471,12 @@ } }, "node_modules/adm-zip": { - "version": "0.5.17", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz", - "integrity": "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.1.tgz", + "integrity": "sha512-Xwrja8nx9e5o2N1my4DsKCeKpdrnACyr1wtbPxBDgGzKzKyE9kRtBFA8mWldI+RVlD7CBZNWY/wQ2+ydwOR6kQ==", "license": "MIT", "engines": { - "node": ">=12.0" + "node": ">=14.0" } }, "node_modules/agent-base": { diff --git a/package.json b/package.json index fa40d6a..7eac90a 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,6 @@ "@react-native/jest-preset": "^0.85.2", "@sentry/react-native": "^8.26.0", "@testing-library/react-native": "^13.3.3", - "@types/adm-zip": "^0.5.8", "@types/jest": "^29.5.14", "@types/node": "^20.19.39", "eslint": "^9.39.5", @@ -65,6 +64,6 @@ "react-native": "*" }, "dependencies": { - "adm-zip": "^0.5.17" + "adm-zip": "^0.6.1" } } From 3325964f3e2b57ec8c23c72b56322745a7f8bd08 Mon Sep 17 00:00:00 2001 From: IronTony Date: Sun, 13 Sep 2026 14:29:07 -0400 Subject: [PATCH 2/2] fix(plugin): match the maven repository url exactly withDocuSignAndroidMavenRepo skipped adding the repository whenever build.gradle contained its URL as a substring. A commented-out declaration or a longer URL with the same prefix counted as present, so the repository was never added. CodeQL flagged the same line as js/incomplete-url-substring-sanitization. The check guards nothing security related, but the substring match was still wrong. The check now reads build.gradle's string literals, skipping // and /* */ comments, and compares each with the repository URL, trailing slashes removed. Comments and strings are matched in one pass, so a glob such as 'libs/*' is not mistaken for the start of a comment, and backslash escapes are honoured, so an escaped quote does not end a string early. Quote style, uri(), trailing comments and multi-line maven blocks still count as declared. The plugin had no tests. Jest now picks up plugin/src, and plugin/tsconfig.build.json excludes *.test.ts so tests stay out of the published plugin/build. Coverage now includes the plugin and reports 49% for plugin/src/index.ts, because the AAR download and strip remain untested. No threshold is set for it. --- CHANGELOG.md | 1 + jest.config.js | 8 +- plugin/src/index.test.ts | 156 +++++++++++++++++++++++++++++++++++++ plugin/src/index.ts | 19 ++++- plugin/tsconfig.build.json | 8 +- 5 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 plugin/src/index.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c110eb7..77a84c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixes - Bump `adm-zip`, the Config Plugin's only runtime dependency, to 0.6.1. This clears GHSA-xcpc-8h2w-3j85 (high) and GHSA-vwc7-r8mq-g2x9 (moderate) from consumers' `npm audit`. Neither was reachable: the plugin never extracts to disk, and it only parses the AAR it downloaded after checking its SHA-256, or its own cached copy under `node_modules`. +- **Android**: the Config Plugin matches the maven repository URL exactly when deciding whether `android/build.gradle` already declares it, and ignores declarations inside `//` and `/* */` comments. The substring check it replaces skipped adding the repository when the URL appeared only in a comment or as the prefix of a longer URL. It was also the CodeQL `js/incomplete-url-substring-sanitization` alert. ## 2.0.0 diff --git a/jest.config.js b/jest.config.js index 796539b..04ea9e9 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,12 +1,18 @@ /** @type {import('jest').Config} */ module.exports = { preset: 'jest-expo', - testMatch: ['/src/**/*.test.ts', '/src/**/*.test.tsx'], + testMatch: [ + '/src/**/*.test.ts', + '/src/**/*.test.tsx', + '/plugin/src/**/*.test.ts', + ], setupFiles: ['/jest.setup.js'], collectCoverageFrom: [ 'src/**/*.{ts,tsx}', '!src/**/*.test.{ts,tsx}', '!src/DocuSignModule.ts', + 'plugin/src/**/*.ts', + '!plugin/src/**/*.test.ts', ], coverageThreshold: { 'src/useDocuSignSigning.ts': { diff --git a/plugin/src/index.test.ts b/plugin/src/index.test.ts new file mode 100644 index 0000000..183f920 --- /dev/null +++ b/plugin/src/index.test.ts @@ -0,0 +1,156 @@ +import { describe, expect, it } from '@jest/globals'; +import type { ExportedConfig } from 'expo/config-plugins'; + +import withDocuSign, { type DocuSignPluginProps } from './index'; + +const DOCUSIGN_REPO = 'https://docucdn-a.akamaihd.net/prod/docusignandroidsdk'; +const CUSTOM_REPO = 'https://maven.example.com/docusign'; + +function buildProjectGradle(repositories: string[]): string { + return [ + 'buildscript {', + ' repositories {', + ' google()', + ' }', + '}', + '', + 'allprojects {', + ' repositories {', + ...repositories.map((line) => ` ${line}`), + ' google()', + ' mavenCentral()', + ' }', + '}', + '', + ].join('\n'); +} + +async function applyProjectBuildGradleMods( + contents: string, + props?: DocuSignPluginProps, +): Promise { + const config: ExportedConfig = withDocuSign( + { name: 'app', slug: 'app' }, + props, + ); + const mod = config.mods?.android?.projectBuildGradle; + if (!mod) { + throw new Error('withDocuSign registered no projectBuildGradle mod'); + } + const result = await mod({ + ...config, + modRawConfig: config, + modResults: { path: 'android/build.gradle', language: 'groovy', contents }, + modRequest: { + projectRoot: '/app', + platformProjectRoot: '/app/android', + modName: 'projectBuildGradle', + platform: 'android', + introspect: false, + }, + }); + return result.modResults.contents; +} + +function countOccurrences(contents: string, needle: string): number { + return contents.split(needle).length - 1; +} + +describe('withDocuSign android maven repository', () => { + it('adds the DocuSign repository inside allprojects.repositories', async () => { + const contents = await applyProjectBuildGradleMods(buildProjectGradle([])); + + expect(contents).toContain( + `allprojects {\n repositories {\n maven { url "${DOCUSIGN_REPO}" }`, + ); + }); + + it('adds androidMavenRepo instead of the DocuSign repository when provided', async () => { + const contents = await applyProjectBuildGradleMods(buildProjectGradle([]), { + androidMavenRepo: CUSTOM_REPO, + }); + + expect(contents).toContain(`maven { url "${CUSTOM_REPO}" }`); + expect(countOccurrences(contents, DOCUSIGN_REPO)).toBe(0); + }); + + it.each([ + ['a double-quoted url', `maven { url "${DOCUSIGN_REPO}" }`], + ['a single-quoted url', `maven { url '${DOCUSIGN_REPO}' }`], + ['a uri() assignment', `maven { url = uri("${DOCUSIGN_REPO}") }`], + ['a trailing slash', `maven { url "${DOCUSIGN_REPO}/" }`], + ['a multi-line block', `maven {\n url "${DOCUSIGN_REPO}"\n }`], + ])( + 'does not add the repository when build.gradle declares it with %s', + async (_label, declaration) => { + const contents = await applyProjectBuildGradleMods( + buildProjectGradle([declaration]), + ); + + expect(countOccurrences(contents, DOCUSIGN_REPO)).toBe(1); + }, + ); + + it('does not add the repository when a declaration is followed by a line comment', async () => { + const contents = await applyProjectBuildGradleMods( + buildProjectGradle([`maven { url "${DOCUSIGN_REPO}" } // DocuSign SDK`]), + ); + + expect(countOccurrences(contents, DOCUSIGN_REPO)).toBe(1); + }); + + it('does not add the repository when an earlier string on its line contains an escaped quote', async () => { + const contents = await applyProjectBuildGradleMods( + buildProjectGradle([ + `def note = "a\\"b"; maven { url "${DOCUSIGN_REPO}" }`, + ]), + ); + + expect(countOccurrences(contents, DOCUSIGN_REPO)).toBe(1); + }); + + it('does not treat comment markers inside strings as comments', async () => { + const contents = await applyProjectBuildGradleMods( + buildProjectGradle([ + "flatDir { dirs 'libs/*' }", + `maven { url "${DOCUSIGN_REPO}" }`, + "flatDir { dirs 'vendor/**/' }", + ]), + ); + + expect(countOccurrences(contents, DOCUSIGN_REPO)).toBe(1); + }); + + it.each([ + ['a line comment', `// maven { url "${DOCUSIGN_REPO}" }`], + ['a block comment', `/* maven { url "${DOCUSIGN_REPO}" } */`], + [ + 'a multi-line block comment', + `/*\n maven { url "${DOCUSIGN_REPO}" }\n */`, + ], + ])( + 'adds the repository when the only declaration is inside %s', + async (_label, declaration) => { + const contents = await applyProjectBuildGradleMods( + buildProjectGradle([declaration]), + ); + + expect(countOccurrences(contents, DOCUSIGN_REPO)).toBe(2); + }, + ); + + it('adds the repository when build.gradle only declares a longer url with the same prefix', async () => { + const contents = await applyProjectBuildGradleMods( + buildProjectGradle([`maven { url "${DOCUSIGN_REPO}/legacy" }`]), + ); + + expect(countOccurrences(contents, DOCUSIGN_REPO)).toBe(2); + }); + + it('leaves build.gradle unchanged when the plugin runs again', async () => { + const firstRun = await applyProjectBuildGradleMods(buildProjectGradle([])); + const secondRun = await applyProjectBuildGradleMods(firstRun); + + expect(secondRun).toBe(firstRun); + }); +}); diff --git a/plugin/src/index.ts b/plugin/src/index.ts index ab465b2..0f7f52b 100644 --- a/plugin/src/index.ts +++ b/plugin/src/index.ts @@ -80,6 +80,23 @@ const withDocuSignAndroidPermissions: ConfigPlugin = (config) => { }); }; +function normalizeRepoUrl(url: string): string { + return url.trim().replace(/\/+$/, ''); +} + +const GRADLE_COMMENT_OR_STRING = + /\/\*[\s\S]*?\*\/|\/\/[^\n]*|"((?:[^"\\\n]|\\.)*)"|'((?:[^'\\\n]|\\.)*)'/g; + +function hasMavenRepo(contents: string, repo: string): boolean { + const target = normalizeRepoUrl(repo); + return Array.from(contents.matchAll(GRADLE_COMMENT_OR_STRING)).some( + ([, doubleQuoted, singleQuoted]) => { + const literal = doubleQuoted ?? singleQuoted; + return literal !== undefined && normalizeRepoUrl(literal) === target; + }, + ); +} + const withDocuSignAndroidMavenRepo: ConfigPlugin = ( config, props, @@ -97,7 +114,7 @@ const withDocuSignAndroidMavenRepo: ConfigPlugin = ( return cfg; } - if (cfg.modResults.contents.includes(repo)) { + if (hasMavenRepo(cfg.modResults.contents, repo)) { return cfg; } diff --git a/plugin/tsconfig.build.json b/plugin/tsconfig.build.json index 8d6a27e..e01a568 100644 --- a/plugin/tsconfig.build.json +++ b/plugin/tsconfig.build.json @@ -8,5 +8,11 @@ "sourceMap": true }, "include": ["./src"], - "exclude": ["**/__mocks__/*", "**/__tests__/*", "build", "node_modules"] + "exclude": [ + "**/__mocks__/*", + "**/__tests__/*", + "**/*.test.ts", + "build", + "node_modules" + ] }