Pin deepmerge-ts in the npm install the workspace override cannot reach

The image builds two dependency trees. `pnpm install` builds the app tree and
honours `pnpm-workspace.yaml`. A second, npm-owned install builds
`/opt/prisma-cli` for the migration CLI, and it reads none of that file.

Only the first was pinned. `prisma@7.8.0` resolves `@prisma/config@7.8.0`,
which resolves `deepmerge-ts@7.1.5`, so the Trivy image scan reported
CVE-2026-40345 (HIGH, stack exhaustion on recursive merges) against
`/opt/prisma-cli` on a commit whose dependency audit over the app tree was
green. The pin read as complete because the check that could see the gap runs
against the artefact, and the one that ran against the lockfile could not.

`overrides` is npm's own mechanism, so `npm pkg set` writes it into the
package.json `npm init -y` just created, before anything resolves. Rebuilding
this exact install both ways: without the field `deepmerge-ts@7.1.5` lands,
with it `8.0.1` lands and `prisma --version` still reports 7.8.0.

The guard beside it fails when the two files name one package at two ranges,
and asserts a non-zero match count first, so a matcher that stops matching
fails loudly instead of passing vacuously. It compares declared ranges only —
a new vulnerable transitive that neither file mentions remains something only
the image scan can see, and that job runs on main-push, not on pull requests.
This commit is contained in:
Marc-André Bombeck committed 2026-08-21 13:31:49 +02:00
1 parent 5d3d512e82
commit 2e5ec7f49f
2 files changed
+119

No files matched your search

+18
View File
@@ -157,9 +157,27 @@ COPY --from=builder /app/src/generated ./src/generated
# the maintenance scripts already shipped in the standalone tree. The Prisma
# config loads dotenv from /app, so expose the isolated copy there before
# stripping npm/Corepack and their caches from the runtime surface.
#
# This install belongs to npm, not pnpm, so the `overrides` block in
# `pnpm-workspace.yaml` does not reach it: a package pinned there for the app
# tree still arrives here at whatever version Prisma asks for. That is not
# hypothetical. `prisma@7.8.0` resolves `@prisma/config@7.8.0`, which resolves
# `deepmerge-ts@7.1.5`, and the image scan reported CVE-2026-40345 (HIGH,
# stack exhaustion on recursive merges) against `/opt/prisma-cli` on a commit
# whose dependency audit over the app tree was green. Two installs, two
# override mechanisms, one of them set. `overrides` is npm's own mechanism and
# `npm pkg set` writes it into the package.json that `npm init -y` just
# created, before anything resolves.
#
# Measured by rebuilding this exact install both ways: without the field
# `deepmerge-ts@7.1.5` lands, with it `8.0.1` lands and `prisma --version`
# 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.
RUN mkdir -p /opt/prisma-cli && \
cd /opt/prisma-cli && \
npm init -y && \
npm pkg set 'overrides.deepmerge-ts=^8.0.0' && \
npm install --omit=dev prisma@7.8 @prisma/engines@7.8 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 && \
@@ -0,0 +1,101 @@
/**
* The two override mechanisms agree, or the build fails here.
*
* The image carries two independent installs. `pnpm install` builds the app
* tree and honours the `overrides` block in `pnpm-workspace.yaml`. A second,
* npm-owned install builds `/opt/prisma-cli` (Dockerfile) and honours nothing
* from that file — it reads its own `overrides` field instead.
*
* On 2026-08-21 only the first one was set. The dependency audit over the app
* tree passed, and the Trivy image scan reported CVE-2026-40345 against
* `deepmerge-ts@7.1.5` under `/opt/prisma-cli`, pulled in by
* `prisma -> @prisma/config`. A pin that covers one of two installs reads
* exactly like a pin that covers both, right up until something scans the
* artefact rather than the lockfile.
*
* So: every package pinned in BOTH places must be pinned to the same range,
* and the Dockerfile's pin must survive as long as the workspace one does.
* This does NOT assert that every workspace override is mirrored — most exist
* for packages `/opt/prisma-cli` never installs, and demanding a mirror there
* would encode noise. It asserts agreement where both files speak, which is
* the failure that actually happened.
*
* 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
* main-push, not on pull requests.
*/
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
const ROOT = process.cwd();
const DOCKERFILE = readFileSync(join(ROOT, "Dockerfile"), "utf8");
const WORKSPACE = readFileSync(join(ROOT, "pnpm-workspace.yaml"), "utf8");
/** `npm pkg set 'overrides.<name>=<range>'` occurrences in the Dockerfile. */
function dockerfileOverrides(): Map<string, string> {
const found = new Map<string, string>();
const pattern = /npm\s+pkg\s+set\s+['"]overrides\.([^='"]+)=([^'"]+)['"]/g;
for (const match of DOCKERFILE.matchAll(pattern)) {
found.set(match[1].trim(), match[2].trim());
}
return found;
}
/**
* `"<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.
*/
function workspaceOverrides(): Map<string, string> {
const found = new Map<string, string>();
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());
}
return found;
}
describe("dependency overrides — pnpm workspace and the npm install agree", () => {
it("finds the Dockerfile's npm-side overrides at all", () => {
const docker = dockerfileOverrides();
// A zero-match regex would make every assertion below vacuously true.
// This is the check that keeps the guard honest about its own matcher.
expect(docker.size).toBeGreaterThan(0);
expect(docker.has("deepmerge-ts")).toBe(true);
});
it("finds the workspace overrides at all", () => {
const workspace = workspaceOverrides();
expect(workspace.size).toBeGreaterThan(0);
expect(workspace.has("deepmerge-ts")).toBe(true);
});
it("pins the same range wherever both files name the same package", () => {
const docker = dockerfileOverrides();
const workspace = workspaceOverrides();
const shared = [...docker.keys()].filter((name) => workspace.has(name));
expect(shared.length).toBeGreaterThan(0);
for (const name of shared) {
expect(
docker.get(name),
`Dockerfile pins ${name} to ${docker.get(name)} while ` +
`pnpm-workspace.yaml pins it to ${workspace.get(name)}. The image ` +
`carries both installs, so the looser of the two is what ships.`,
).toBe(workspace.get(name));
}
});
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.
const range = dockerfileOverrides().get("deepmerge-ts");
expect(range).toBeDefined();
expect(range).toMatch(/\^?8\./);
});
});