chore(deps): raise the stale advisory floors and pin valibot

Three overrides had drifted behind the advisories they were written for,
and the scan found the packages at versions the pins were supposed to
exclude.

dompurify moves from <=3.4.10 to <3.4.13. GHSA-55q2-fjhq-7xh7 is fixed
in 3.4.13 and the tree carried 3.4.12. The copy is jspdf's optional HTML
renderer, an optionalDependency at ^3.3.1, so the raise stays inside the
range jspdf declares.

postcss moves from <8.5.10 to <8.5.23 for CVE-2026-69153. next declares
an exact postcss@8.4.31, so nothing but this bound moves it; the build
has run on the 8.5 line since the first bump and stays there.

valibot gets a floor it never had, <1.4.2, for CVE-2026-59952. Two
copies arrive and neither is ours to call: an optional peer of
@hookform/resolvers, which this app does not use because it validates
with zod, and prisma's local dev server. 1.4.2 is the same major, so no
consumer is moved onto a new one.

@hono/node-server moves from <1.19.13 to <1.19.15 for
GHSA-frvp-7c67-39w9. The app tree already resolved 1.19.17, so this
changes nothing here; it exists so the bound states the real floor and
so the Dockerfile mirror has something correct to match.

The floor test in ci-determinism asserted the exact key and replacement
string, which meant every legitimate raise failed it while a floor gone
stale against a newer advisory passed it unchanged. It now reduces each
key to a package name and asserts the pin is at or above the fixed
version, with a case proving the comparison can return false.
This commit is contained in:
Marc-André Bombeck committed 2026-09-02 15:46:55 +02:00
1 parent c2ea07b0bc
commit f62cd59789
3 files changed
+114 -36

No files matched your search

+17 -26
View File
@@ -5,10 +5,10 @@ settings:
excludeLinksFromLockfile: false
overrides:
dompurify@<=3.4.10: ^3.4.11
postcss@<8.5.10: ^8.5.10
dompurify@<3.4.13: ^3.4.13
postcss@<8.5.23: ^8.5.23
nanoid@<3.3.18: ^3.3.18
'@hono/node-server@<1.19.13': ^1.19.13
'@hono/node-server@<1.19.15': ^1.19.15
hono@<4.12.25: ^4.12.25
picomatch@<4.0.4: ^4.0.4
body-parser@<2.3.0: ^2.3.0
@@ -21,6 +21,7 @@ overrides:
sharp@<0.35.0: ^0.35.3
pdfjs-dist@>=5.6.83 <6.2.108: 6.2.108
deepmerge-ts@<8.0.0: ^8.0.0
valibot@<1.4.2: ^1.4.2
importers:
@@ -40,7 +41,7 @@ importers:
version: 3.2.2(react@19.2.7)
'@hookform/resolvers':
specifier: ^5.9.1
version: 5.9.1(@standard-schema/spec@1.1.0)(ajv-formats@2.1.1(ajv@8.20.0))(ajv@8.20.0)(effect@3.20.0)(react-hook-form@7.83.0(react@19.2.7))(valibot@1.2.0(typescript@6.0.3))(zod@4.4.3)
version: 5.9.1(@standard-schema/spec@1.1.0)(ajv-formats@2.1.1(ajv@8.20.0))(ajv@8.20.0)(effect@3.20.0)(react-hook-form@7.83.0(react@19.2.7))(valibot@1.4.2(typescript@6.0.3))(zod@4.4.3)
'@modelcontextprotocol/sdk':
specifier: ^1.30.0
version: 1.30.0(@cfworker/json-schema@4.1.1)(supports-color@7.2.0)(zod@4.4.3)
@@ -864,7 +865,7 @@ packages:
react-hook-form: ^7.55.0
superstruct: '>=0.12.0'
typanion: ^3.3.2
valibot: '>=0.31.0 || ^1.0.0-beta.4 || ^1.0.0-rc'
valibot: ^1.4.2
vest: '>=6.0.0'
yup: ^1.0.0
zod: ^3.25.0 || ^4.0.0
@@ -4204,8 +4205,8 @@ packages:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'}
dompurify@3.4.12:
resolution: {integrity: sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==}
dompurify@3.4.14:
resolution: {integrity: sha512-dVoH9z+MY+C9IilgGCk3YfFqjLi3fChm2OiKJMzh6axrJ5qwxqWaZamgmHrpv22CN/KdbZJuGEGgfQoL00LTdg==}
dot-prop@6.0.1:
resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==}
@@ -5961,10 +5962,6 @@ packages:
resolution: {integrity: sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==}
engines: {node: '>=4'}
postcss@8.5.20:
resolution: {integrity: sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==}
engines: {node: ^10 || ^12 || >=14}
postcss@8.5.25:
resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==}
engines: {node: ^10 || ^12 || >=14}
@@ -6923,8 +6920,8 @@ packages:
utrie@1.0.2:
resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==}
valibot@1.2.0:
resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==}
valibot@1.4.2:
resolution: {integrity: sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==}
peerDependencies:
typescript: '>=5'
peerDependenciesMeta:
@@ -7923,7 +7920,7 @@ snapshots:
dependencies:
hono: 4.13.3
'@hookform/resolvers@5.9.1(@standard-schema/spec@1.1.0)(ajv-formats@2.1.1(ajv@8.20.0))(ajv@8.20.0)(effect@3.20.0)(react-hook-form@7.83.0(react@19.2.7))(valibot@1.2.0(typescript@6.0.3))(zod@4.4.3)':
'@hookform/resolvers@5.9.1(@standard-schema/spec@1.1.0)(ajv-formats@2.1.1(ajv@8.20.0))(ajv@8.20.0)(effect@3.20.0)(react-hook-form@7.83.0(react@19.2.7))(valibot@1.4.2(typescript@6.0.3))(zod@4.4.3)':
dependencies:
'@standard-schema/utils': 0.3.0
react-hook-form: 7.83.0(react@19.2.7)
@@ -7932,7 +7929,7 @@ snapshots:
ajv: 8.20.0
ajv-formats: 2.1.1(ajv@8.20.0)
effect: 3.20.0
valibot: 1.2.0(typescript@6.0.3)
valibot: 1.4.2(typescript@6.0.3)
zod: 4.4.3
'@humanfs/core@0.19.1': {}
@@ -8894,7 +8891,7 @@ snapshots:
proper-lockfile: 4.1.2
remeda: 2.33.4
std-env: 3.10.0
valibot: 1.2.0(typescript@6.0.3)
valibot: 1.4.2(typescript@6.0.3)
zeptomatch: 2.1.0
transitivePeerDependencies:
- typescript
@@ -11068,7 +11065,7 @@ snapshots:
dependencies:
esutils: 2.0.3
dompurify@3.4.12:
dompurify@3.4.14:
optionalDependencies:
'@types/trusted-types': 2.0.7
optional: true
@@ -12253,7 +12250,7 @@ snapshots:
optionalDependencies:
canvg: 3.0.11
core-js: 3.49.0
dompurify: 3.4.12
dompurify: 3.4.14
html2canvas: 1.4.1
jsx-ast-utils@3.3.5:
@@ -12603,7 +12600,7 @@ snapshots:
'@swc/helpers': 0.5.15
baseline-browser-mapping: 2.10.43
caniuse-lite: 1.0.30001805
postcss: 8.5.20
postcss: 8.5.26
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
styled-jsx: 5.1.6(@babel/core@7.29.7(supports-color@7.2.0))(react@19.2.7)
@@ -12990,12 +12987,6 @@ snapshots:
cssesc: 3.0.0
util-deprecate: 1.0.2
postcss@8.5.20:
dependencies:
nanoid: 3.3.18
picocolors: 1.1.1
source-map-js: 1.2.1
postcss@8.5.25:
dependencies:
nanoid: 3.3.18
@@ -14169,7 +14160,7 @@ snapshots:
base64-arraybuffer: 1.0.2
optional: true
valibot@1.2.0(typescript@6.0.3):
valibot@1.4.2(typescript@6.0.3):
optionalDependencies:
typescript: 6.0.3
+21 -3
View File
@@ -1,8 +1,20 @@
overrides:
"dompurify@<=3.4.10": "^3.4.11"
"postcss@<8.5.10": "^8.5.10"
# GHSA-55q2-fjhq-7xh7: a crafted document gets past the sanitiser and its
# markup survives into the output. The copy here is jspdf's optional HTML
# renderer, which the doctor-report PDF path reaches.
"dompurify@<3.4.13": "^3.4.13"
# CVE-2026-69153. Next declares an exact `postcss@8.4.31`, so nothing except
# this bound moves it. The build has run on the 8.5 line since the first
# bump; the fix landed in 8.5.23.
"postcss@<8.5.23": "^8.5.23"
"nanoid@<3.3.18": "^3.3.18"
"@hono/node-server@<1.19.13": "^1.19.13"
# GHSA-frvp-7c67-39w9 raised the floor from 1.19.13 to 1.19.15. Mirrored for
# /opt/prisma-cli in the Dockerfile, which is where the flagged 1.19.11
# actually landed: prisma pulls it through @prisma/dev, and that install
# reads npm `overrides`, not this file. The bound stays on the 1.x line
# because the advisory is fixed there too and 2.x is a major nothing asked
# for.
"@hono/node-server@<1.19.15": "^1.19.15"
"hono@<4.12.25": "^4.12.25"
"picomatch@<4.0.4": "^4.0.4"
"body-parser@<2.3.0": "^2.3.0"
@@ -39,6 +51,12 @@ overrides:
# override is the only way through. It runs on the config-loading and CLI
# path, not under a request, but a red audit blocks the whole release train.
"deepmerge-ts@<8.0.0": "^8.0.0"
# CVE-2026-59952. Two copies arrive, neither of them ours to call: an
# optional peer of `@hookform/resolvers` (this app validates with zod) and
# prisma's local dev server. Mirrored for /opt/prisma-cli in the Dockerfile,
# where the flagged 1.2.0 landed. 1.4.2 is the same major, so no consumer is
# dragged onto a new one.
"valibot@<1.4.2": "^1.4.2"
allowBuilds:
"@prisma/engines": true
+76 -7
View File
@@ -150,17 +150,86 @@ describe("container dependency installation", () => {
});
});
/**
* `>= 0` comparison over a dotted version, enough for the floor checks below.
* Pre-release suffixes are not expected in an override replacement and are
* ignored rather than guessed at.
*/
function atLeast(version: string, floor: string): boolean {
const parts = (value: string) =>
value
.replace(/^[\^~>=<\s]+/, "")
.split("-")[0]
.split(".")
.map((piece) => Number.parseInt(piece, 10) || 0);
const [a, b] = [parts(version), parts(floor)];
for (let i = 0; i < Math.max(a.length, b.length); i += 1) {
const left = a[i] ?? 0;
const right = b[i] ?? 0;
if (left !== right) return left > right;
}
return true;
}
describe("production dependency advisory floors", () => {
it("pins vulnerable transitive ranges to compatible patched versions", () => {
/**
* The floor an advisory demands, by package. Raising an override past one of
* these must not break this test: an earlier version of it asserted the exact
* key and replacement string, so every legitimate floor raise failed it while
* a floor that had gone STALE against a newer advisory passed. It now asserts
* the property that matters, which is that the pin is at or above the fixed
* version. Update a floor here when a new advisory raises it.
*/
const floors: Record<string, string> = {
// GHSA-55q2-fjhq-7xh7, reached through jspdf's optional HTML renderer.
dompurify: "3.4.13",
// CVE-2026-69153.
postcss: "8.5.23",
// GHSA-frvp-7c67-39w9 and CVE-2026-39406. Mirrored in the Dockerfile.
"@hono/node-server": "1.19.15",
hono: "4.12.25",
// CVE-2026-59952. Mirrored in the Dockerfile.
valibot: "1.4.2",
};
it("pins vulnerable transitive ranges at or above the fixed version", () => {
const workspace = parse(readRepoFile("pnpm-workspace.yaml")) as {
overrides?: Record<string, string>;
};
const overrides = workspace.overrides ?? {};
expect(workspace.overrides).toMatchObject({
"dompurify@<=3.4.10": "^3.4.11",
"postcss@<8.5.10": "^8.5.10",
"@hono/node-server@<1.19.13": "^1.19.13",
"hono@<4.12.25": "^4.12.25",
});
const byName = new Map<string, string>();
for (const [key, replacement] of Object.entries(overrides)) {
const selectorAt = key.startsWith("@")
? key.indexOf("@", 1)
: key.indexOf("@");
byName.set(
selectorAt === -1 ? key : key.slice(0, selectorAt),
replacement,
);
}
expect(byName.size).toBeGreaterThan(0);
for (const [name, floor] of Object.entries(floors)) {
const replacement = byName.get(name);
expect(
replacement,
`${name} has no override in pnpm-workspace.yaml`,
).toBeDefined();
expect(
atLeast(replacement as string, floor),
`${name} is pinned to ${replacement}, below the advisory floor ${floor}.`,
).toBe(true);
}
});
it("compares versions rather than always agreeing", () => {
// The comparison is the whole test above; a helper that never returns
// false would make it vacuous.
expect(atLeast("^3.4.13", "3.4.13")).toBe(true);
expect(atLeast("^3.4.14", "3.4.13")).toBe(true);
expect(atLeast("^3.4.12", "3.4.13")).toBe(false);
expect(atLeast("^1.19.9", "1.19.15")).toBe(false);
expect(atLeast("^2.0.5", "1.19.15")).toBe(true);
});
});