From f7b2b51da98125c218efa6e4c6484d72ffdd7be1 Mon Sep 17 00:00:00 2001 From: le king fu Date: Thu, 24 Sep 2026 20:40:36 -0400 Subject: [PATCH 1/2] fix(security): bump @react-navigation/core to 7.22.1 via override @react-navigation/core <= 7.22.0 depends on query-string, which pulls decode-uri-component <= 0.4.2 (GHSA-vcc3-ghjq-m6fr, DoS on malformed percent-encoded input). 7.22.1 parses queries itself and drops the dependency. A scoped override bumps it without touching anything else: the lock diff is limited to @react-navigation/core and /routers. Add smoke-test guards for the failure mode of the superseded PR #118 (blanket npm audit fix): babel-preset-expo and @expo/metro-config must resolve from the root node_modules, only one Expo SDK major may be installed, every metro copy must stay on 0.83.x. Replayed against the PR #118 lock, all four checks fail. Also add a deep link round trip through getStateFromPath/getPathFromState with query params. Fixes #117 Co-Authored-By: Claude Opus 5.5 --- package-lock.json | 15 ++++---- package.json | 3 +- tests/smoke.test.cjs | 92 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 702db88..45f1979 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3468,16 +3468,15 @@ } }, "node_modules/@react-navigation/core": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.14.0.tgz", - "integrity": "sha512-tMpzskBzVp0E7CRNdNtJIdXjk54Kwe/TF9ViXAef+YFM1kSfGv4e/B2ozfXE+YyYgmh4WavTv8fkdJz1CNyu+g==", + "version": "7.22.1", + "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.22.1.tgz", + "integrity": "sha512-6JAaYM4u6RPtVMUjN5ovX5DNBdBZtDqebk7kEwlncUP6fP4P94+qEYv1bRmtxSFs7UxiZwQQs+jzTsBaW2WeyA==", "license": "MIT", "dependencies": { - "@react-navigation/routers": "^7.5.3", + "@react-navigation/routers": "^7.6.4", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", - "query-string": "^7.1.3", "react-is": "^19.1.0", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" @@ -3546,9 +3545,9 @@ } }, "node_modules/@react-navigation/routers": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.5.3.tgz", - "integrity": "sha512-1tJHg4KKRJuQ1/EvJxatrMef3NZXEPzwUIUZ3n1yJ2t7Q97siwRtbynRpQG9/69ebbtiZ8W3ScOZF/OmhvM4Rg==", + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.6.4.tgz", + "integrity": "sha512-GI7eJm8/KsZUQaYcXvEExikKurRZRgEsSzyZ7faENfi65yqJBCXjDMwyN1pF6pNW1MoLH1ErDwDivFxY6BzD3w==", "license": "MIT", "dependencies": { "nanoid": "^3.3.11" diff --git a/package.json b/package.json index 40d4077..83619e6 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,8 @@ "metro-source-map": "^0.83.8", "metro-symbolicate": "^0.83.8", "metro-transform-plugins": "^0.83.8", - "metro-transform-worker": "^0.83.8" + "metro-transform-worker": "^0.83.8", + "@react-navigation/core": "^7.22.1" }, "private": true } diff --git a/tests/smoke.test.cjs b/tests/smoke.test.cjs index 5073b9b..f1ac57b 100644 --- a/tests/smoke.test.cjs +++ b/tests/smoke.test.cjs @@ -168,6 +168,98 @@ check('feedback.ts stays import-free (protects type-stripping test path)', () => ); }); +// --- Build toolchain guards --- +// A blanket `npm audit fix` (PR #118) once rewrote hundreds of lock entries: +// babel-preset-expo and @expo/metro-config lost their root hoisting and a +// second Expo SDK was nested inside Expo 54. The tests stayed green while the +// first Metro bundle failed. These checks lock the layout the build relies on. + +const ROOT = path.resolve(__dirname, '..'); +const ROOT_NODE_MODULES = path.join(ROOT, 'node_modules'); + +function majorOf(version) { + return version.split('.')[0]; +} + +// Every installed copy of `name`, at the root and nested under other packages. +function installedCopies(name, dir = ROOT_NODE_MODULES, found = []) { + const pkgJson = path.join(dir, name, 'package.json'); + if (fs.existsSync(pkgJson)) { + found.push({ dir: path.dirname(pkgJson), version: require(pkgJson).version }); + } + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + if (!entry.isDirectory() || entry.name.startsWith('.')) continue; + const pkgDirs = entry.name.startsWith('@') + ? fs.readdirSync(path.join(dir, entry.name)).map((n) => path.join(dir, entry.name, n)) + : [path.join(dir, entry.name)]; + for (const pkgDir of pkgDirs) { + const nested = path.join(pkgDir, 'node_modules'); + if (fs.existsSync(nested)) installedCopies(name, nested, found); + } + } + return found; +} + +// babel.config.js and sql-transformer.js require these from the repo root. +for (const request of ['babel-preset-expo', '@expo/metro-config/babel-transformer']) { + check(`${request} resolves from the repo root node_modules`, () => { + const resolved = require.resolve(request, { paths: [ROOT] }); + const pkgName = request.startsWith('@') + ? request.split('/').slice(0, 2).join('/') + : request.split('/')[0]; + const expectedDir = path.join(ROOT_NODE_MODULES, pkgName) + path.sep; + assert.ok( + resolved.startsWith(expectedDir), + `${request} resolved to ${resolved}, expected under ${expectedDir}` + ); + }); +} + +check('a single Expo SDK major is installed (matches package.json)', () => { + const pkg = require('../package.json'); + const expected = majorOf(pkg.dependencies.expo.replace(/^[~^]/, '')); + const copies = installedCopies('expo'); + assert.ok(copies.length > 0, 'expo is not installed'); + const strays = copies.filter((c) => majorOf(c.version) !== expected); + assert.deepEqual( + strays.map((c) => `${c.version} at ${path.relative(ROOT, c.dir)}`), + [], + `expected only expo ${expected}.x` + ); +}); + +check('every installed metro copy honors the 0.83.x override pin', () => { + const copies = installedCopies('metro'); + assert.ok(copies.length > 0, 'metro is not installed'); + const strays = copies.filter((c) => !/^0\.83\./.test(c.version)); + assert.deepEqual( + strays.map((c) => `${c.version} at ${path.relative(ROOT, c.dir)}`), + [], + 'metro must stay on 0.83.x' + ); +}); + +// @react-navigation/core <= 7.22.0 pulls query-string -> decode-uri-component +// <= 0.4.2 (GHSA-vcc3-ghjq-m6fr). 7.22.1 parses queries itself, so the deep +// link round trip must still carry query params through. +check('@react-navigation/core is >= 7.22.1 and drops query-string', () => { + const core = require('@react-navigation/core/package.json'); + const [major, minor, patch] = core.version.split('.').map(Number); + assert.ok( + major > 7 || (major === 7 && (minor > 22 || (minor === 22 && patch >= 1))), + `@react-navigation/core ${core.version} is vulnerable` + ); + assert.ok(!('query-string' in (core.dependencies || {})), 'core still depends on query-string'); +}); + +check('@react-navigation/core deep link round trip keeps query params', () => { + const { getStateFromPath, getPathFromState } = require('@react-navigation/core'); + const config = { screens: { Task: 'task/:id' } }; + const state = getStateFromPath('/task/abc?x=1&y=a%20b', config); + assert.deepEqual(state.routes[0].params, { id: 'abc', x: '1', y: 'a b' }); + assert.equal(getPathFromState(state, config), '/task/abc?x=1&y=a%20b'); +}); + if (failed === 0) { console.log('\nsmoke OK'); process.exit(0); -- 2.45.2 From 2117cc501692c6351705a617b0a14357a2866b40 Mon Sep 17 00:00:00 2001 From: le king fu Date: Fri, 25 Sep 2026 19:59:17 -0400 Subject: [PATCH 2/2] test: clarify deep link round trip scope (expo-router fork not covered) Refs #117 Co-Authored-By: Claude Opus 5.5 --- tests/smoke.test.cjs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/smoke.test.cjs b/tests/smoke.test.cjs index f1ac57b..483e328 100644 --- a/tests/smoke.test.cjs +++ b/tests/smoke.test.cjs @@ -242,6 +242,10 @@ check('every installed metro copy honors the 0.83.x override pin', () => { // @react-navigation/core <= 7.22.0 pulls query-string -> decode-uri-component // <= 0.4.2 (GHSA-vcc3-ghjq-m6fr). 7.22.1 parses queries itself, so the deep // link round trip must still carry query params through. +// Scope: this covers @react-navigation/core's own linking helpers only. The +// app's deep links (simplliste://task/) are resolved by expo-router's fork +// (expo-router/build/fork/getStateFromPath*.js), which still uses +// query-string@7 (see #120) and is not exercised here. check('@react-navigation/core is >= 7.22.1 and drops query-string', () => { const core = require('@react-navigation/core/package.json'); const [major, minor, patch] = core.version.split('.').map(Number); -- 2.45.2