Skip to content

Commit 3c9b6da

Browse files
Address vulnerabilities and update CI to handle acceptable vulnerabilities
1 parent c3fce08 commit 3c9b6da

5 files changed

Lines changed: 175 additions & 40 deletions

File tree

.github/workflows/portal-frontend-ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ jobs:
4141
run: pnpm install --frozen-lockfile
4242

4343
- name: Audit production dependencies
44-
run: pnpm audit --prod --audit-level high
44+
run: pnpm security:audit
4545

4646
- name: Lint
4747
run: pnpm lint
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Frontend dependency risk acceptance
2+
3+
Recorded: 2026-07-29
4+
Review by: 2026-10-27
5+
6+
## Current audit status
7+
8+
After upgrading all safely compatible patched dependencies:
9+
10+
- `pnpm audit` reports three high-severity vulnerability instances across two advisories.
11+
- `pnpm audit --prod` reports one high-severity advisory.
12+
- No critical, moderate, or low-severity findings remain.
13+
14+
## GHSA-qwww-vcr4-c8h2 — React Router RSC CSRF
15+
16+
- Affected dependency: `react-router@7.18.2`, through `react-router-dom@7.18.2`.
17+
- Available remediation: `react-router@8.3.0`.
18+
- Applicability: Not applicable to the current application architecture. The advisory states that
19+
it affects applications using unstable React Server Components APIs. This frontend is a
20+
client-rendered Vite SPA and does not use React Server Components or React Router server actions.
21+
- Reason upgrade is deferred: A matching stable `react-router-dom@8.3.0` release is not currently
22+
published. Overriding its internal `react-router` dependency across a major version is not a
23+
supported or safe remediation.
24+
- Remediation trigger: Upgrade the React Router packages together when compatible stable releases
25+
are available, or reassess immediately if the application adopts RSC APIs or server actions.
26+
- CI enforcement: `pnpm security:audit` accepts only this advisory on the exact
27+
`react-router-dom > react-router` dependency path and continues to fail for every other high or
28+
critical production advisory.
29+
30+
## GHSA-mh99-v99m-4gvg / CVE-2026-14257 — brace-expansion memory exhaustion
31+
32+
- Affected dependencies: `brace-expansion@1.1.16` through ESLint tooling and
33+
`brace-expansion@2.1.2` through Vitest coverage tooling.
34+
- Available remediation: `brace-expansion@5.0.8`.
35+
- Exposure: These paths originate from declared development tools and are absent from
36+
`pnpm audit --prod`. They are not included in the production browser application.
37+
- Reason upgrade is deferred: The patched release is an ESM-only major version. Forcing it into
38+
consumers that require the 1.x or 2.x APIs may break linting and test tooling.
39+
- Compensating control: Do not pass attacker-controlled glob or brace patterns to lint, test, or
40+
coverage commands.
41+
- Remediation trigger: Remove this exception when ESLint, Vitest, or their transitive dependencies
42+
adopt a compatible patched release.
43+
44+
## Review requirements
45+
46+
Re-run `pnpm audit` and `pnpm audit --prod` during dependency updates and at the review date above.
47+
Reassess this acceptance earlier if dependency paths, application rendering architecture, or CI
48+
input trust boundaries change.

portal/frontend/package.json

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"test:coverage": "vitest run --coverage",
1616
"format": "prettier . --write",
1717
"format:check": "prettier . --check",
18+
"security:audit": "node scripts/audit-production-dependencies.mjs",
1819
"security:verify": "node scripts/verify-production-security.mjs",
1920
"preview": "vite preview"
2021
},
@@ -26,7 +27,7 @@
2627
"react": "19.2.4",
2728
"react-dom": "19.2.4",
2829
"react-i18next": "16.6.2",
29-
"react-router-dom": "7.15.1"
30+
"react-router-dom": "7.18.2"
3031
},
3132
"devDependencies": {
3233
"@eslint/eslintrc": "3.3.5",
@@ -58,12 +59,13 @@
5859
"pnpm": {
5960
"overrides": {
6061
"yaml@>=1.0.0 <1.10.3": "1.10.3",
61-
"brace-expansion@<1.1.13": "1.1.13",
62-
"brace-expansion@>=2.0.0 <2.0.3": "2.0.3",
62+
"brace-expansion@<1.1.16": "1.1.16",
63+
"brace-expansion@>=2.0.0 <2.1.2": "2.1.2",
64+
"brace-expansion@>=5.0.0 <5.0.8": "5.0.8",
6365
"picomatch@>=4.0.0 <4.0.4": "4.0.4",
64-
"postcss@<8.5.10": "8.5.10",
66+
"postcss@<8.5.18": "8.5.18",
6567
"esbuild@>=0.27.3 <0.28.1": "0.28.1",
66-
"js-yaml@>=4.0.0 <=4.1.1": "4.2.0",
68+
"js-yaml@>=4.0.0 <4.3.0": "4.3.0",
6769
"ws@>=8.0.0 <8.21.0": "8.21.0",
6870
"@babel/core@<=7.29.0": "7.29.6"
6971
}

portal/frontend/pnpm-lock.yaml

Lines changed: 35 additions & 34 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
*/
6+
7+
import { spawnSync } from 'node:child_process'
8+
9+
const allowedAdvisories = new Map([
10+
[
11+
'GHSA-qwww-vcr4-c8h2',
12+
{
13+
description: 'React Router unstable RSC APIs are not used by this Vite SPA',
14+
pathPattern: /^\. > react-router-dom@[^ ]+ > react-router@[^ ]+$/,
15+
},
16+
],
17+
])
18+
19+
const pnpmCli = process.env.npm_execpath
20+
21+
if (!pnpmCli) {
22+
throw new Error('Unable to locate the active pnpm CLI from npm_execpath')
23+
}
24+
25+
const audit = spawnSync(process.execPath, [pnpmCli, 'audit', '--prod', '--json'], {
26+
cwd: process.cwd(),
27+
encoding: 'utf8',
28+
shell: false,
29+
})
30+
31+
if (audit.error) {
32+
throw new Error(`Unable to run pnpm audit: ${audit.error.message}`)
33+
}
34+
35+
let report
36+
37+
try {
38+
report = JSON.parse(audit.stdout)
39+
} catch {
40+
const details = [audit.stdout, audit.stderr].filter(Boolean).join('\n').trim()
41+
throw new Error(`Unable to parse pnpm audit output${details ? `:\n${details}` : ''}`)
42+
}
43+
44+
if (audit.status !== 0 && audit.status !== 1) {
45+
throw new Error(`pnpm audit failed with exit code ${String(audit.status)}:\n${audit.stderr}`)
46+
}
47+
48+
const advisories = Object.values(report.advisories ?? {})
49+
const blockingAdvisories = []
50+
const acceptedAdvisories = []
51+
52+
for (const advisory of advisories) {
53+
if (advisory.severity === 'high' || advisory.severity === 'critical') {
54+
const exception = allowedAdvisories.get(advisory.github_advisory_id)
55+
const paths = advisory.findings?.flatMap((finding) => finding.paths ?? []) ?? []
56+
const pathsMatch = paths.length > 0 && paths.every((path) => exception?.pathPattern.test(path))
57+
58+
if (exception && pathsMatch) {
59+
acceptedAdvisories.push({ advisory, description: exception.description })
60+
} else {
61+
blockingAdvisories.push(advisory)
62+
}
63+
}
64+
}
65+
66+
for (const { advisory, description } of acceptedAdvisories) {
67+
console.warn(
68+
`Accepted documented advisory ${advisory.github_advisory_id}: ${advisory.title}\n` +
69+
`Reason: ${description}\n`,
70+
)
71+
}
72+
73+
if (blockingAdvisories.length > 0) {
74+
const summary = blockingAdvisories
75+
.map(
76+
(advisory) =>
77+
`- ${advisory.github_advisory_id ?? advisory.id}: ${advisory.title} (${advisory.severity})`,
78+
)
79+
.join('\n')
80+
81+
throw new Error(`Production dependency audit found blocking advisories:\n${summary}`)
82+
}
83+
84+
console.log('Production dependency audit passed.')

0 commit comments

Comments
 (0)