security(deps): fix npm audit vulnerabilities (nodemailer CRLF, path-to-regexp ReDoS)
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 18m58s
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 18m58s
Resolves 7 npm audit findings (3 moderate, 4 high) via `npm audit fix`
— no --force needed, all bumps satisfied by existing semver ranges:
basic-ftp 5.2.0 -> 5.2.2 (high: FTP command injection via CRLF)
brace-expansion 1.1.12 -> 1.1.13 (moderate: ReDoS / mem exhaustion)
nodemailer 8.0.3 -> 8.0.5 (high: SMTP command injection via
CRLF in EHLO/HELO transport name,
GHSA-vvjj-xcjg-gr5g, and envelope.size
injection GHSA-c7w3-x93f-qmm8)
path-to-regexp 8.3.0 -> 8.4.2 (high: ReDoS, GHSA-j3q9-mxjg-w52f and
GHSA-27v5-c462-wpq7)
picomatch 4.0.3 -> 4.0.4 (high: method injection + ReDoS)
vite 0.115.0 -> 0.124.0 (high: path traversal / FS bypass,
dev-only, transitive via vitest)
yaml 2.x -> patched (moderate: stack overflow, dev-only)
Only package-lock.json changed — no source changes required, no API
breaks. nodemailer 8.0.5 is fully backwards-compatible with our usage
in src/services/email.ts.
Adds src/__tests__/no-vulnerable-deps.test.ts as a TDD regression guard:
runs `npm audit --omit=dev --json` and asserts
metadata.vulnerabilities.high === 0 && critical === 0. Network failures
are skipped rather than failing CI. Red→Green verified locally (stashed
lockfile -> 2 high failures; restored -> 0).
Test count: 901 -> 902 (new regression guard). npm audit: 4 high -> 0.
This commit is contained in:
parent
6d7cf14a4f
commit
2186747940
2 changed files with 207 additions and 123 deletions
91
src/__tests__/no-vulnerable-deps.test.ts
Normal file
91
src/__tests__/no-vulnerable-deps.test.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
/**
|
||||
* Regression guard for npm audit vulnerabilities.
|
||||
*
|
||||
* Runs `npm audit --omit=dev --json` and asserts that no high or critical
|
||||
* severity vulnerabilities exist in the production dependency tree.
|
||||
*
|
||||
* Rationale: after fixing CVEs (nodemailer CRLF GHSA-vvjj-xcjg-gr5g,
|
||||
* path-to-regexp ReDoS GHSA-j3q9-mxjg-w52f / GHSA-27v5-c462-wpq7, and
|
||||
* related transitive issues in basic-ftp, brace-expansion, picomatch),
|
||||
* we want CI to fail fast if a new high/critical vuln is introduced into
|
||||
* the production dependency graph rather than discovering it later.
|
||||
*
|
||||
* Network access is required (audit hits the npm registry). If the
|
||||
* registry is unreachable in a given environment, the test is skipped
|
||||
* rather than reported as a failure — we don't want flaky network to
|
||||
* break builds. All other errors (including the audit finding vulns)
|
||||
* must fail loudly.
|
||||
*/
|
||||
describe("npm audit regression guard", () => {
|
||||
it(
|
||||
"has zero high or critical vulnerabilities in production dependencies",
|
||||
() => {
|
||||
let stdout: string;
|
||||
try {
|
||||
// `npm audit` exits non-zero when vulnerabilities are found, which
|
||||
// makes execSync throw. We still want to parse stdout in that case,
|
||||
// so we catch and inspect the error object.
|
||||
stdout = execSync("npm audit --omit=dev --json", {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
timeout: 60_000,
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
const e = err as { stdout?: Buffer | string; stderr?: Buffer | string; message?: string };
|
||||
const out = typeof e.stdout === "string" ? e.stdout : e.stdout?.toString() ?? "";
|
||||
const errOut = typeof e.stderr === "string" ? e.stderr : e.stderr?.toString() ?? "";
|
||||
|
||||
// No JSON at all? Likely a network/registry failure — skip, don't fail CI.
|
||||
if (!out || !out.trim().startsWith("{")) {
|
||||
const msg = (errOut || e.message || "").toLowerCase();
|
||||
if (
|
||||
msg.includes("enotfound") ||
|
||||
msg.includes("etimedout") ||
|
||||
msg.includes("econnrefused") ||
|
||||
msg.includes("network") ||
|
||||
msg.includes("registry")
|
||||
) {
|
||||
console.warn("npm audit: registry unreachable, skipping regression guard");
|
||||
return;
|
||||
}
|
||||
throw new Error(
|
||||
`npm audit did not return parseable JSON. stderr=${errOut} message=${e.message}`,
|
||||
);
|
||||
}
|
||||
stdout = out;
|
||||
}
|
||||
|
||||
const report = JSON.parse(stdout) as {
|
||||
metadata?: {
|
||||
vulnerabilities?: {
|
||||
info?: number;
|
||||
low?: number;
|
||||
moderate?: number;
|
||||
high?: number;
|
||||
critical?: number;
|
||||
total?: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const vulns = report.metadata?.vulnerabilities;
|
||||
expect(vulns, "npm audit JSON missing metadata.vulnerabilities").toBeDefined();
|
||||
|
||||
const high = vulns?.high ?? 0;
|
||||
const critical = vulns?.critical ?? 0;
|
||||
|
||||
if (high > 0 || critical > 0) {
|
||||
// Surface the full report so failure output is actionable.
|
||||
console.error("npm audit found high/critical vulnerabilities:");
|
||||
console.error(JSON.stringify(vulns, null, 2));
|
||||
}
|
||||
|
||||
expect(critical).toBe(0);
|
||||
expect(high).toBe(0);
|
||||
},
|
||||
90_000,
|
||||
);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue