mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-15 19:52:50 +03:00
* chore(repo): re-untrack the _tasks self-referential symlink
`caf768e3c4` untracked it; the DeepAI merge (44069c5f54, #9443) re-added
it. It is an absolute symlink pointing at one machine's checkout, and
AGENTS.md keeps `_tasks/` out of the main repo entirely. While tracked,
`check-tracked-artifacts.mjs` fails on pre-commit, so no commit can be
made on this branch at all — this restores the precedent fix purely to
unblock committing, and is unrelated to the Docker change that follows.
* fix(docker): eliminate npm-bundled CVEs from the published image
Trivy reported 9 HIGH/MEDIUM CVEs against the npm CLI's own bundled
node_modules inside the published image (brace-expansion, ip-address,
tar, undici under /usr/local/lib/node_modules/npm/node_modules).
The base stage claimed `npm install -g npm@latest` shipped patched
copies. It does not: npm@12.0.2 (latest) bundles brace-expansion 5.0.7,
ip-address 10.2.0, tar 7.5.19 and undici 6.27.0 — all still vulnerable.
No npm release fixes them, so that step was buying zero CVEs.
Overlay the patched versions onto npm's bundled tree instead, pinned and
semver-compatible with the ranges npm's own tree declares (minimatch ->
brace-expansion ^5.0.5, socks -> ip-address ^10.1.1, node-gyp -> tar
^7.5.4 and undici ^6.25.0, so undici stays on 6.x). Removing npm from
the runner stages was not viable — the app shells out to npm at runtime
(installers/utils.ts::runNpm, system/autoUpdate.ts,
system/globalPackagePath.ts, api/system/version) — and the old comment
asserting otherwise is corrected.
---------
Co-authored-by: backryun <bakryun0718@proton.me>
134 lines
5.3 KiB
TypeScript
134 lines
5.3 KiB
TypeScript
/**
|
|
* Trivy image scan reported 9 HIGH/MEDIUM CVEs against the npm CLI's own
|
|
* *bundled* node_modules inside the published image:
|
|
*
|
|
* usr/local/lib/node_modules/npm/node_modules/brace-expansion CVE-2026-69152, CVE-2026-14257
|
|
* usr/local/lib/node_modules/npm/node_modules/ip-address CVE-2026-69192, CVE-2026-69198, CVE-2026-54272
|
|
* usr/local/lib/node_modules/npm/node_modules/tar GHSA-r292-9mhp-454m
|
|
* usr/local/lib/node_modules/npm/node_modules/undici CVE-2026-16729, CVE-2026-16728, CVE-2026-15157
|
|
*
|
|
* The `base` stage used to claim `npm install -g npm@latest` shipped patched
|
|
* copies. That was false: npm@12.0.2 (the latest release at the time) bundles
|
|
* brace-expansion 5.0.7, ip-address 10.2.0, tar 7.5.19 and undici 6.27.0 — every
|
|
* one still vulnerable. No npm release fixes these, so the Dockerfile now
|
|
* overlays the patched versions onto npm's bundled tree.
|
|
*
|
|
* Removing npm from the runner stages was NOT viable: npm is invoked at runtime
|
|
* by src/lib/services/installers/utils.ts::runNpm (embedded services),
|
|
* src/lib/system/autoUpdate.ts, src/lib/system/globalPackagePath.ts and
|
|
* src/app/api/system/version/route.ts.
|
|
*
|
|
* This guards the mechanism (the overlay exists, targets all four packages, and
|
|
* pins versions at or above the fixed ones). The end-to-end proof is a clean
|
|
* Trivy scan on the next published image — this sandbox has no Docker daemon.
|
|
*/
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
const dockerfile = fs.readFileSync(path.join(repoRoot, "Dockerfile"), "utf-8");
|
|
|
|
/** Dockerfile source with line continuations joined, the way the shell sees it. */
|
|
const joined = dockerfile.replace(/\\\n\s*/g, " ");
|
|
/** Instruction lines only — comments must never satisfy these assertions. */
|
|
const instructions = joined
|
|
.split("\n")
|
|
.filter((l) => !l.trim().startsWith("#"))
|
|
.join("\n");
|
|
|
|
/** package -> lowest version that is not affected by the reported CVEs. */
|
|
const FIXED_MINIMUMS: Record<string, [number, number, number]> = {
|
|
"brace-expansion": [5, 0, 9],
|
|
"ip-address": [10, 3, 1],
|
|
tar: [7, 5, 21],
|
|
undici: [6, 28, 0],
|
|
};
|
|
|
|
function parseVersion(raw: string): [number, number, number] {
|
|
const parts = raw.split(".").map((n) => Number.parseInt(n, 10));
|
|
assert.equal(parts.length, 3, `expected an exact x.y.z pin, got "${raw}"`);
|
|
assert.ok(
|
|
parts.every((n) => Number.isInteger(n)),
|
|
`expected an exact x.y.z pin, got "${raw}"`
|
|
);
|
|
return [parts[0], parts[1], parts[2]];
|
|
}
|
|
|
|
function isAtLeast(actual: [number, number, number], min: [number, number, number]): boolean {
|
|
for (let i = 0; i < 3; i++) {
|
|
if (actual[i] > min[i]) return true;
|
|
if (actual[i] < min[i]) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
test("base stage pins patched versions of every CVE-flagged npm-bundled package", () => {
|
|
for (const [pkg, min] of Object.entries(FIXED_MINIMUMS)) {
|
|
const match = new RegExp(`\\b${pkg}@(\\d+\\.\\d+\\.\\d+)\\b`).exec(instructions);
|
|
assert.ok(
|
|
match,
|
|
`Dockerfile must install an explicit patched ${pkg}@x.y.z for npm's bundled tree`
|
|
);
|
|
const actual = parseVersion(match[1]);
|
|
assert.ok(
|
|
isAtLeast(actual, min),
|
|
`${pkg}@${match[1]} is below the fixed version ${min.join(".")} — the Trivy alert would return`
|
|
);
|
|
}
|
|
});
|
|
|
|
test("undici stays on the 6.x line node-gyp declares (^6.25.0), never 8.x", () => {
|
|
const match = /\bundici@(\d+)\.\d+\.\d+\b/.exec(instructions);
|
|
assert.ok(match, "Dockerfile must pin an undici version");
|
|
assert.equal(
|
|
match[1],
|
|
"6",
|
|
"npm's bundled node-gyp declares undici ^6.25.0 — an 8.x overlay would break its resolution"
|
|
);
|
|
});
|
|
|
|
test("the patched copies actually replace npm's bundled ones", () => {
|
|
for (const pkg of Object.keys(FIXED_MINIMUMS)) {
|
|
assert.match(
|
|
instructions,
|
|
new RegExp(`for pkg in [^;]*\\b${pkg}\\b`),
|
|
`${pkg} must be part of the overlay loop that rewrites npm's bundled node_modules`
|
|
);
|
|
}
|
|
assert.match(
|
|
instructions,
|
|
/rm -rf "\/usr\/local\/lib\/node_modules\/npm\/node_modules\/\$pkg"/,
|
|
"the overlay must remove the vulnerable bundled copy before replacing it"
|
|
);
|
|
assert.match(
|
|
instructions,
|
|
/cp -R "\/tmp\/npm-cve-patch\/node_modules\/\$pkg"\s+"\/usr\/local\/lib\/node_modules\/npm\/node_modules\/\$pkg"/,
|
|
"the overlay must copy the patched package into npm's bundled node_modules"
|
|
);
|
|
assert.match(
|
|
instructions,
|
|
/test -d "\/usr\/local\/lib\/node_modules\/npm\/node_modules\/\$pkg"/,
|
|
"the overlay must fail the build loudly if npm's layout changes and a target path disappears"
|
|
);
|
|
});
|
|
|
|
test("the overlay smoke-tests npm after patching it", () => {
|
|
assert.match(
|
|
instructions,
|
|
/npm --version/,
|
|
"the patched npm must be exercised in the same layer so a broken overlay fails the build"
|
|
);
|
|
});
|
|
|
|
test("the stale 'npm is not invoked at runtime' claim is gone", () => {
|
|
assert.doesNotMatch(
|
|
dockerfile,
|
|
/npm is not invoked at runtime/,
|
|
"npm IS invoked at runtime (installers/utils.ts::runNpm, system/autoUpdate.ts, " +
|
|
"system/globalPackagePath.ts, api/system/version) — the comment must not claim otherwise"
|
|
);
|
|
});
|