Merge pull request #894 from MBombeck/chore/scanning-sweep

chore(security): close seven scanning alerts and the guard that hid one
This commit is contained in:
Marc-André Bombeck authored and GitHub committed 2026-09-02 17:00:26 +02:00
commit a0f2e0d260
7 files changed
+212 -42

No files matched your search

+8 -2
View File
@@ -2,14 +2,20 @@ name: Auto-merge Dependabot
on: pull_request
# Nothing outside the one job needs more than a checkout, so the default for
# the workflow is read-only and the write scopes sit on the job that spends
# them: `gh pr merge --auto` writes to the repository's merge queue state, and
# both the merge and the review comment write to the pull request.
permissions:
contents: write
pull-requests: write
contents: read
jobs:
auto-merge:
runs-on: ubuntu-latest
if: github.actor == 'dependabot[bot]'
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+25
View File
@@ -15,6 +15,31 @@
base URL that is not a URL at all behaves as it did. The operator card now
says which providers the field accepts. Found in a fork by @sweidinger.
### Security
- **The published image no longer ships four flagged dependency versions.**
Three floors in the override block had gone stale: the sanitiser behind the
doctor-report PDF sat below the fix for GHSA-55q2-fjhq-7xh7, the CSS
processor the build runs on sat below the fix for CVE-2026-69153, and a
schema library that arrives through two unrelated parents had no floor at
all (CVE-2026-59952). All three are raised and re-resolved, each inside its
existing major, so nothing changes shape for anyone.
- **The Prisma command line inside the image gets the same pins as the app.**
The image carries two separate installs, and only one of them reads the
project's override block. The other is an npm install of the Prisma CLI, and
it was quietly resolving an HTTP server (GHSA-frvp-7c67-39w9,
CVE-2026-39406) and a schema library (CVE-2026-59952) at the versions the
image scan flagged, while the same packages were pinned and clean in the
application tree. Both are now pinned on the npm side too, verified by
rebuilding that install both ways. This is the second time the same gap has
produced a finding, so the guard that compares the two files was widened: it
had been skipping every scoped package name, which is exactly the case that
went unnoticed here.
- The Dependabot auto-merge workflow no longer hands a write token to the
whole run. The workflow default is read-only and the write scopes sit on the
one job that spends them. Nothing about which updates merge automatically
changes.
## [1.38.3] — 2026-09-02
An account with two-factor can be deleted from the phone, and every data
+10
View File
@@ -191,11 +191,21 @@ COPY --from=builder /app/src/generated ./src/generated
# still reports 7.8.0. Keep the range in step with the entry in
# `pnpm-workspace.yaml` — they are one decision applied to two package
# managers, and a fix that lands in only one of them is the defect above.
#
# The same class recurred on 2026-09-02: the image scan reported
# `@hono/node-server@1.19.11` (GHSA-frvp-7c67-39w9, CVE-2026-39406) and
# `valibot@1.2.0` (CVE-2026-59952) under this path, both reached through
# `prisma -> @prisma/dev`, while the app tree carried 1.19.17 and a pin the
# npm install never reads. Rebuilding this install both ways confirms it:
# without the two fields below npm resolves 1.19.11 and 1.2.0, with them it
# resolves 1.19.17 and 1.4.2, and `prisma --version` still reports 7.8.0.
RUN mkdir -p /opt/prisma-cli && \
cd /opt/prisma-cli && \
npm init -y && \
npm pkg set 'overrides.deepmerge-ts=^8.0.0' && \
npm pkg set 'overrides.mysql2=^3.22.0' && \
npm pkg set 'overrides.@hono/node-server=^1.19.15' && \
npm pkg set 'overrides.valibot=^1.4.2' && \
npm install --omit=dev prisma@7.8.0 @prisma/engines@7.8.0 tsx@4.23.1 dotenv@17.4.2 && \
ln -sfn /opt/prisma-cli/node_modules/.bin/tsx /usr/local/bin/healthlog-tsx && \
ln -sfn /opt/prisma-cli/node_modules/dotenv /app/node_modules/dotenv && \
+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);
});
});
@@ -20,6 +20,12 @@
* would encode noise. It asserts agreement where both files speak, which is
* the failure that actually happened.
*
* The matcher had a blind spot of its own until 2026-09-02: it refused any
* workspace key beginning with `@`, so every scoped package was invisible to
* it. `@hono/node-server` was pinned on one side and unpinned on the other and
* the guard stayed green, which is the same shape of green the guard exists to
* prevent. Scoped keys parse now, and a test below pins that they do.
*
* Limit, written down so the next reader does not over-trust a green run: this
* compares declared ranges. It cannot see a NEW vulnerable transitive that
* neither file mentions. Only the image scan can, and that job runs on
@@ -44,17 +50,30 @@ function dockerfileOverrides(): Map<string, string> {
return found;
}
/**
* Reduces an overrides key to the bare package name. The key carries a version
* selector, so `deepmerge-ts@<8.0.0` and a bare `deepmerge-ts` both land on the
* same name. A scoped name opens with its own `@`, so the selector starts at
* the SECOND one, not the first.
*/
function packageNameOf(key: string): string {
const trimmed = key.trim();
const selectorAt = trimmed.startsWith("@")
? trimmed.indexOf("@", 1)
: trimmed.indexOf("@");
return (selectorAt === -1 ? trimmed : trimmed.slice(0, selectorAt)).trim();
}
/**
* `"<name>@<selector>": "<range>"` entries under the workspace `overrides:`
* block. The key carries a version selector, so `deepmerge-ts@<8.0.0` and a
* bare `deepmerge-ts` both have to reduce to the same package name.
* block.
*/
function workspaceOverrides(): Map<string, string> {
const found = new Map<string, string>();
const pattern = /^\s+"([^"@][^"]*?)(?:@[^"]*)?"\s*:\s*"([^"]+)"/gm;
const pattern = /^\s+"([^"]+)"\s*:\s*"([^"]+)"/gm;
const block = WORKSPACE.split(/^allowBuilds:/m)[0];
for (const match of block.matchAll(pattern)) {
found.set(match[1].trim(), match[2].trim());
found.set(packageNameOf(match[1]), match[2].trim());
}
return found;
}
@@ -74,6 +93,23 @@ describe("dependency overrides — pnpm workspace and the npm install agree", ()
expect(workspace.has("deepmerge-ts")).toBe(true);
});
it("reads scoped workspace keys rather than skipping them", () => {
// The old matcher started the name at `[^"@]`, so `@hono/node-server` was
// never in the shared set and could disagree across the two files without
// failing anything. Both halves are asserted: the reduction itself, and
// that a real scoped entry survives it.
expect(packageNameOf("@hono/node-server@<1.19.15")).toBe(
"@hono/node-server",
);
expect(packageNameOf("dompurify@<3.4.13")).toBe("dompurify");
expect(packageNameOf("js-yaml@>=4.0.0 <4.3.1")).toBe("js-yaml");
expect(packageNameOf("deepmerge-ts")).toBe("deepmerge-ts");
const workspace = workspaceOverrides();
const scoped = [...workspace.keys()].filter((name) => name.startsWith("@"));
expect(scoped.length).toBeGreaterThan(0);
});
it("pins the same range wherever both files name the same package", () => {
const docker = dockerfileOverrides();
const workspace = workspaceOverrides();
@@ -91,6 +127,21 @@ describe("dependency overrides — pnpm workspace and the npm install agree", ()
}
});
it("mirrors every pin the /opt/prisma-cli tree was scanned on", () => {
// These three were each reported by the image scan under
// `/opt/prisma-cli`, reached through `prisma`. Losing a mirror is not a
// style regression, it is the vulnerable version coming back.
const docker = dockerfileOverrides();
for (const name of ["deepmerge-ts", "@hono/node-server", "valibot"]) {
expect(
docker.has(name),
`${name} was flagged under /opt/prisma-cli and needs an ` +
`\`npm pkg set 'overrides.${name}=...'\` line in the Dockerfile; ` +
`pnpm-workspace.yaml does not reach that install.`,
).toBe(true);
}
});
it("keeps the override ahead of the version Prisma's config asks for", () => {
// `@prisma/config` requests deepmerge-ts 7.x; the advisory is fixed in
// 8.0.0. Anything that lets a 7.x back in re-opens CVE-2026-40345.