mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
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:
@@ -201,15 +201,50 @@ jobs:
|
|||||||
if path and not info.get('dev') and not info.get('devOptional'):
|
if path and not info.get('dev') and not info.get('devOptional'):
|
||||||
prod.add(path.split('node_modules/')[-1])
|
prod.add(path.split('node_modules/')[-1])
|
||||||
vulns = data.get('vulnerabilities', {})
|
vulns = data.get('vulnerabilities', {})
|
||||||
|
# Documented advisory exceptions: high/critical findings whose only offered
|
||||||
|
# 'fix' is a semver-major change and which do not apply to how Bambuddy ships.
|
||||||
|
# Keyed by GHSA id; RE-REVIEW ON EVERY react-router BUMP.
|
||||||
|
# GHSA-qwww-vcr4-c8h2 - React Router RSC-mode CSRF. Bambuddy is a Vite SPA
|
||||||
|
# using BrowserRouter with no RSC runtime (@react-router/server is NOT
|
||||||
|
# installed), so the vulnerable code path is unreachable. No non-major fix
|
||||||
|
# exists (7.18.1 is the most-patched 7.x - it clears 14 other advisories that
|
||||||
|
# older 7.x carry - and the RSC fix landed only in the 8.3.0 major). react-router
|
||||||
|
# /-dom are pinned to 7.18.1 in package.json. If a non-major fix ships, this stops
|
||||||
|
# being exempt (major-only guard below) and the gate fails until we take it.
|
||||||
|
ALLOWLIST = {'GHSA-qwww-vcr4-c8h2'}
|
||||||
|
def advisory_ids(name, seen=None):
|
||||||
|
seen = seen if seen is not None else set()
|
||||||
|
if name in seen:
|
||||||
|
return set()
|
||||||
|
seen.add(name)
|
||||||
|
ids = set()
|
||||||
|
for item in vulns.get(name, {}).get('via', []):
|
||||||
|
if isinstance(item, dict):
|
||||||
|
url = item.get('url', '')
|
||||||
|
if '/advisories/' in url:
|
||||||
|
ids.add(url.rsplit('/', 1)[-1])
|
||||||
|
elif isinstance(item, str):
|
||||||
|
ids |= advisory_ids(item, seen)
|
||||||
|
return ids
|
||||||
|
def fix_is_major(v):
|
||||||
|
fa = v.get('fixAvailable')
|
||||||
|
return isinstance(fa, dict) and fa.get('isSemVerMajor')
|
||||||
|
def exempt(name, v):
|
||||||
|
ids = advisory_ids(name)
|
||||||
|
return bool(ids) and ids <= ALLOWLIST and fix_is_major(v)
|
||||||
fixable = {n: v for n, v in vulns.items()
|
fixable = {n: v for n, v in vulns.items()
|
||||||
if n in prod and v.get('severity') in ('high', 'critical') and v.get('fixAvailable')}
|
if n in prod and v.get('severity') in ('high', 'critical')
|
||||||
|
and v.get('fixAvailable') and not exempt(n, v)}
|
||||||
skipped = len(vulns) - len({n: v for n, v in vulns.items() if n in prod})
|
skipped = len(vulns) - len({n: v for n, v in vulns.items() if n in prod})
|
||||||
if fixable:
|
if fixable:
|
||||||
for name, v in fixable.items():
|
for name, v in fixable.items():
|
||||||
print(f'FIXABLE {v[\"severity\"].upper()}: {name}')
|
print(f'FIXABLE {v[\"severity\"].upper()}: {name}')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
total = sum(1 for n, v in vulns.items() if n in prod and v.get('severity') in ('high', 'critical'))
|
total = sum(1 for n, v in vulns.items() if n in prod and v.get('severity') in ('high', 'critical'))
|
||||||
|
exempted = sorted(n for n, v in vulns.items() if n in prod and exempt(n, v))
|
||||||
print(f'npm audit: {total} high/critical (0 fixable), {len(vulns)} total ({skipped} npm-internal filtered)')
|
print(f'npm audit: {total} high/critical (0 fixable), {len(vulns)} total ({skipped} npm-internal filtered)')
|
||||||
|
if exempted:
|
||||||
|
print('exempted (documented, unreachable): ' + ', '.join(exempted))
|
||||||
"
|
"
|
||||||
|
|
||||||
frontend-typecheck:
|
frontend-typecheck:
|
||||||
|
|||||||
@@ -308,13 +308,46 @@ jobs:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const vulns = results.vulnerabilities || {};
|
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 filtered = {};
|
||||||
|
const flagged = {};
|
||||||
for (const [name, info] of Object.entries(vulns)) {
|
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;
|
results.vulnerabilities = filtered;
|
||||||
fs.writeFileSync('npm-audit-results.json', JSON.stringify(results, null, 2));
|
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
|
console.log(count > 0
|
||||||
? count + ' production vulnerabilities found'
|
? count + ' production vulnerabilities found'
|
||||||
: 'No production vulnerabilities (filtered ' + Object.keys(vulns).length + ' npm-internal entries)');
|
: 'No production vulnerabilities (filtered ' + Object.keys(vulns).length + ' npm-internal entries)');
|
||||||
|
|||||||
Generated
+12
-12
@@ -33,7 +33,7 @@
|
|||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-i18next": "^16.3.5",
|
"react-i18next": "^16.3.5",
|
||||||
"react-markdown": "^9.1.0",
|
"react-markdown": "^9.1.0",
|
||||||
"react-router-dom": "^7.16.0",
|
"react-router-dom": "7.18.1",
|
||||||
"react-simple-keyboard": "^3.8.164",
|
"react-simple-keyboard": "^3.8.164",
|
||||||
"recharts": "^3.5.1",
|
"recharts": "^3.5.1",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
@@ -3615,15 +3615,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "5.0.7",
|
"version": "5.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
|
||||||
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
|
"integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^4.0.2"
|
"balanced-match": "^4.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "18 || 20 || >=22"
|
"node": "20 || >=22"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/browserslist": {
|
"node_modules/browserslist": {
|
||||||
@@ -6965,9 +6965,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.12",
|
"version": "3.3.16",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
|
||||||
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -7185,9 +7185,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.15",
|
"version": "8.5.23",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz",
|
||||||
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
"integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -7204,7 +7204,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.12",
|
"nanoid": "^3.3.16",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-i18next": "^16.3.5",
|
"react-i18next": "^16.3.5",
|
||||||
"react-markdown": "^9.1.0",
|
"react-markdown": "^9.1.0",
|
||||||
"react-router-dom": "^7.16.0",
|
"react-router-dom": "7.18.1",
|
||||||
"react-simple-keyboard": "^3.8.164",
|
"react-simple-keyboard": "^3.8.164",
|
||||||
"recharts": "^3.5.1",
|
"recharts": "^3.5.1",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
@@ -48,8 +48,9 @@
|
|||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"minimatch": "^10.2.1",
|
"minimatch": "^10.2.1",
|
||||||
"brace-expansion": "^5.0.7",
|
"brace-expansion": "^5.0.8",
|
||||||
"js-yaml": "^4.3.0"
|
"js-yaml": "^4.3.0",
|
||||||
|
"react-router": "7.18.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
|
|||||||
Reference in New Issue
Block a user