fix(security): bump @react-navigation/core to 7.22.1 via override #119

Merged
maximus merged 2 commits from issue-117-react-navigation-core-7.22.1 into master 2026-09-26 00:04:07 +00:00
3 changed files with 101 additions and 9 deletions
Showing only changes of commit f7b2b51da9 - Show all commits

15
package-lock.json generated
View file

@ -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"

View file

@ -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
}

View file

@ -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);