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
+36 -1
View File
@@ -201,15 +201,50 @@ jobs:
if path and not info.get('dev') and not info.get('devOptional'):
prod.add(path.split('node_modules/')[-1])
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()
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})
if fixable:
for name, v in fixable.items():
print(f'FIXABLE {v[\"severity\"].upper()}: {name}')
sys.exit(1)
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)')
if exempted:
print('exempted (documented, unreachable): ' + ', '.join(exempted))
"
frontend-typecheck: