build(docker): mirror the hono and valibot pins into /opt/prisma-cli

The image carries two installs. pnpm builds the app tree from
pnpm-workspace.yaml; npm builds /opt/prisma-cli and reads only its own
overrides field. The scan reported @hono/node-server@1.19.11
(GHSA-frvp-7c67-39w9, CVE-2026-39406) and valibot@1.2.0
(CVE-2026-59952) under that path while both packages were pinned and
clean in the app tree. Same shape as the deepmerge-ts finding, same
cause.

Rebuilt this exact install both ways to confirm rather than assume.
Without the two fields npm resolves @hono/node-server 1.19.11 and
valibot 1.2.0, which are the two versions the scan named. With them it
resolves 1.19.17 and 1.4.2, and prisma --version still reports 7.8.0.
Both arrive through prisma -> @prisma/dev.

The lockstep guard could not have caught this. Its workspace matcher
started a package name at [^"@], so every scoped key was invisible to
it: @hono/node-server was in the workspace file and absent from the
Dockerfile, the shared set never contained it, and the guard stayed
green. Scoped keys parse now, a test pins the reduction itself so the
blind spot cannot return quietly, and a second test asserts that the
three packages the image scan has actually flagged under /opt/prisma-cli
keep their npm-side mirror. Both fail when the pin is removed or moved
out of step.
This commit is contained in:
Marc-André Bombeck committed 2026-09-02 15:47:13 +02:00
1 parent f62cd59789
commit c9f8aee476
2 files changed
+65 -4

No files matched your search

+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 && \
@@ -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.