chore(deps): patch postcss + brace-expansion; pin react-router 7.18.1 with a documented audit exception

- postcss 8.5.15 -> 8.5.23 (GHSA-r28c-9q8g-f849, source-map path traversal)
- brace-expansion override ^5.0.7 -> ^5.0.8 (GHSA-mh99-v99m-4gvg, DoS)

react-router: pin react-router-dom to exact 7.18.1 (direct dep) and react-router
to 7.18.1 via overrides (transitive). 7.18.1 is the most-patched 7.x -- it clears
14 advisories that older 7.x releases carry, several reachable from a SPA (open-
redirect XSS in Link/useNavigate, route-matching DoS). The one remaining advisory,
GHSA-qwww-vcr4-c8h2, is RSC-mode-only; Bambuddy is a Vite SPA using BrowserRouter
with no RSC runtime (@react-router/server not installed), so the path is
unreachable. The only version that fully clears npm audit is the 8.3.0 major
(no react-router-dom 8.x exists; it needs migrating 50 import sites plus a React
peer bump), deferred as its own change.

Because a version pin can't stop npm from reporting the theoretical 7.11.0
downgrade as fixAvailable, the ci.yml (hard) and security.yml (nightly issue)
audit gates gain a narrow, documented allowlist keyed on the GHSA id. It resolves
the react-router-dom -> react-router advisory chain and stays fail-closed: a
different advisory on react-router still fails the gate, and an isSemVerMajor
guard drops the exemption the moment a non-major fix ships, forcing us to take it.
This commit is contained in:
maziggy
2026-07-27 12:29:24 +02:00
parent 3dcbbdd25e
commit 60bf1bbab2
4 changed files with 87 additions and 18 deletions
+35 -2
View File
@@ -308,13 +308,46 @@ jobs:
}
}
const vulns = results.vulnerabilities || {};
// Documented advisory exceptions (keyed by GHSA id) - see ci.yml for the
// full rationale and the matching hard gate. GHSA-qwww-vcr4-c8h2: React
// Router RSC-mode CSRF, not reachable from Bambuddy's BrowserRouter SPA
// (@react-router/server not installed); react-router/-dom pinned to 7.18.1
// (the most-patched 7.x), no non-major fix exists. Auto-surfaces again if a
// non-major fix ships.
const ALLOWLIST = new Set(['GHSA-qwww-vcr4-c8h2']);
function advisoryIds(name, seen) {
seen = seen || new Set();
if (seen.has(name)) return new Set();
seen.add(name);
const ids = new Set();
for (const item of (vulns[name] || {}).via || []) {
if (item && typeof item === 'object') {
const url = item.url || '';
if (url.includes('/advisories/')) ids.add(url.split('/').pop());
} else if (typeof item === 'string') {
for (const id of advisoryIds(item, seen)) ids.add(id);
}
}
return ids;
}
function fixIsMajor(info) {
const fa = info.fixAvailable;
return fa && typeof fa === 'object' && fa.isSemVerMajor;
}
function exempt(name, info) {
const ids = advisoryIds(name);
return ids.size > 0 && [...ids].every(id => ALLOWLIST.has(id)) && fixIsMajor(info);
}
const filtered = {};
const flagged = {};
for (const [name, info] of Object.entries(vulns)) {
if (prodDeps.has(name)) filtered[name] = info;
if (!prodDeps.has(name)) continue;
filtered[name] = info;
if (!exempt(name, info)) flagged[name] = info;
}
results.vulnerabilities = filtered;
fs.writeFileSync('npm-audit-results.json', JSON.stringify(results, null, 2));
const count = Object.keys(filtered).length;
const count = Object.keys(flagged).length;
console.log(count > 0
? count + ' production vulnerabilities found'
: 'No production vulnerabilities (filtered ' + Object.keys(vulns).length + ' npm-internal entries)');