From 05da1b44d1eeb3d001ee7badd1262b5942d1e984 Mon Sep 17 00:00:00 2001 From: goetchstone Date: Fri, 19 Jun 2026 16:37:33 -0400 Subject: [PATCH] fix(deps): nodemailer 9 resolves 6 advisories All 6 nodemailer advisories (one high) are patched in 9. next-auth's nodemailer peer is optional/unused (Credentials-only auth), so an overrides forces the tree to 9; allowlist shrinks to postcss. --- CLAUDE.md | 4 ++-- docs/DECISIONS.md | 10 ++++++++++ package-lock.json | 16 ++++++++-------- package.json | 7 +++++-- scripts/audit-check.mjs | 18 +++++------------- 5 files changed, 30 insertions(+), 25 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index ea9d19e..cb9aa64 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -170,9 +170,9 @@ This is a combined RMS (Resource Management System) backend + marketing site for - **Rate limiting is in-memory:** `server/rate-limit.ts` uses a Map — resets on server restart, doesn't work across multiple instances. Acceptable for single-instance VPS deployment. - **No email queue retry backoff:** `app/api/cron/process-emails/route.ts` retries failed emails up to 3 times with no delay between attempts — should add exponential backoff if email failures become common - **Portal token is permanent:** Client portal tokens (cuid) never expire and can't be rotated without DB manual intervention — add rotation mechanism before handling sensitive client data -- **Accepted-risk dependency vulns (CI-allowlisted):** Several transitive advisories are open with **no fix available** and are deliberately allowlisted in `scripts/audit-check.mjs` — the `npm audit` CI gate still fails on any *new* high/critical advisory, just not these. Re-triage if `EmailPayload` or the SMTP transport options in `server/email/index.ts` ever widen. - - **nodemailer — 6 advisories (incl. high-severity):** GHSA-c7w3-x93f-qmm8 (`envelope.size`), GHSA-vvjj-xcjg-gr5g (transport `name`), GHSA-268h-hp4c-crq3 (`List-*` header CRLF), GHSA-wqvq-jvpq-h66f (`jsonTransport` bypass), GHSA-r7g4-qg5f-qqm2 (OAuth2 token TLS), GHSA-p6gq-j5cr-w38f (`raw` option file-read/SSRF). All unreachable: `sendEmail()` accepts only the `EmailPayload` allowlist (`from/to/subject/html/text/replyTo`) and the transport sets only `host/port/secure/auth{user,pass}` — so we never set `envelope`, transport `name`, `List-*` headers, `raw`, `jsonTransport`, or OAuth2 auth. Fix needs nodemailer 8+, but next-auth's `@auth/core` peer-dep pins us to 7. Revisit when next-auth 5 stable supports nodemailer 8+. +- **Accepted-risk dependency vulns (CI-allowlisted):** One transitive advisory is open with **no fix available** and is deliberately allowlisted in `scripts/audit-check.mjs` — the `npm audit` CI gate still fails on any *new* high/critical advisory, just not this one. - **postcss GHSA-qx2v-qp2m-jg93** (moderate, XSS in CSS stringify) — transitive via Next.js's bundled postcss; not exploitable through our surface (no untrusted input to postcss stringify). Resolves when Next bumps its bundled postcss. +- **nodemailer forced to 9 via `overrides`:** the 6 nodemailer advisories (incl. one high) were *resolved* by upgrading to nodemailer 9. next-auth's `@auth/core` declares an **optional** `nodemailer@^7` peer (only for its Email provider, which we don't use — auth is Credentials-only in `server/auth/index.ts`), so `package.json` `overrides` forces the whole tree to 9 — runtime-safe, no `--legacy-peer-deps`. Drop the override when next-auth's peer range includes nodemailer 9+. ### Testing & Deployment diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 7553616..bb7ca41 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -180,3 +180,13 @@ The AI framework is the cleaner reference implementation of the pattern. **Why:** A cluster of nodemailer advisories (one high) has no fix — next-auth's `@auth/core` peer-dep pins nodemailer to 7 — so `--audit-level=high` left the gate permanently red, blocking every PR. Each was triaged unreachable against the actual code (`server/email/index.ts` exposes only the `EmailPayload` allowlist; the transport sets no `envelope`/`name`/`raw`/`jsonTransport`/OAuth2). Allowlisting the specific GHSAs keeps the gate meaningful — a new, unrelated high/critical still fails the build. **Alternatives considered:** lower to `--audit-level=critical` (would silently ignore future *high* vulns — too loose); `|| true` on the step (disables the gate); add `audit-ci`/`better-npm-audit` (a new dependency for what ~40 lines of zero-dep JS does). The allowlist must shrink as fixes land — revisit when next-auth 5 supports nodemailer 8+. + +--- + +### 2026-06-19: Force nodemailer 9 via `overrides` (next-auth's peer is optional) + +**Decision:** `package.json` `overrides` pins the whole tree to nodemailer 9 (matching our direct dep), overriding `@auth/core`'s `nodemailer@^7` peer. + +**Why:** Upgrading nodemailer 7→9 fixes the 6 advisories (one high) that had no other fix, so the allowlist shrinks to postcss only. `@auth/core`'s nodemailer peer is **optional** and used only by next-auth's Email provider — we use the **Credentials** provider only (`server/auth/index.ts`), so next-auth never loads nodemailer. The override is therefore runtime-safe and lets `npm ci` resolve without `--legacy-peer-deps` (which hides real breakage — see LESSONS). + +**Alternatives:** bump next-auth to a beta whose peer allows nodemailer 9 (rejected — bumping the auth library is riskier than a scoped override); `--legacy-peer-deps` in CI (rejected). Remove the override once next-auth's peer range includes nodemailer 9+. diff --git a/package-lock.json b/package-lock.json index b5f66d4..c2b8fe8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,7 @@ "lucide-react": "^1.18.0", "next": "^16.2.9", "next-auth": "^5.0.0-beta.31", - "nodemailer": "^7.0.13", + "nodemailer": "^9.0.1", "pino": "^10.3.1", "prisma": "^6.19.2", "react": "19.2.7", @@ -42,7 +42,7 @@ "@tailwindcss/postcss": "^4", "@types/bcryptjs": "^3.0.0", "@types/node": "^25", - "@types/nodemailer": "^7.0.11", + "@types/nodemailer": "^8.0.1", "@types/pino": "^7.0.5", "@types/react": "^19", "@types/react-dom": "^19", @@ -3461,9 +3461,9 @@ } }, "node_modules/@types/nodemailer": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.11.tgz", - "integrity": "sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-8.0.1.tgz", + "integrity": "sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw==", "dev": true, "license": "MIT", "dependencies": { @@ -8922,9 +8922,9 @@ "license": "MIT" }, "node_modules/nodemailer": { - "version": "7.0.13", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.13.tgz", - "integrity": "sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.1.tgz", + "integrity": "sha512-Gwv8SQewT616ZM/URn0H54b8PWo/Wum7md3EW2aWy1lO27+WZCX+Xyak3J+NlmHUjDh5ME+uesJUDRbR3Ye8Bw==", "license": "MIT-0", "engines": { "node": ">=6.0.0" diff --git a/package.json b/package.json index d4fc9d3..4bf917f 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,9 @@ "prisma": { "seed": "tsx prisma/seed.ts" }, + "overrides": { + "nodemailer": "$nodemailer" + }, "dependencies": { "@auth/prisma-adapter": "^2.11.2", "@base-ui/react": "^1.5.0", @@ -36,7 +39,7 @@ "lucide-react": "^1.18.0", "next": "^16.2.9", "next-auth": "^5.0.0-beta.31", - "nodemailer": "^7.0.13", + "nodemailer": "^9.0.1", "pino": "^10.3.1", "prisma": "^6.19.2", "react": "19.2.7", @@ -53,7 +56,7 @@ "@tailwindcss/postcss": "^4", "@types/bcryptjs": "^3.0.0", "@types/node": "^25", - "@types/nodemailer": "^7.0.11", + "@types/nodemailer": "^8.0.1", "@types/pino": "^7.0.5", "@types/react": "^19", "@types/react-dom": "^19", diff --git a/scripts/audit-check.mjs b/scripts/audit-check.mjs index 31fa447..e92ef9c 100644 --- a/scripts/audit-check.mjs +++ b/scripts/audit-check.mjs @@ -3,8 +3,8 @@ // advisory EXCEPT a small allowlist of triaged, no-fix-available advisories // (documented in CLAUDE.md "Known Issues"). This keeps the gate meaningful — a // new, unrelated high/critical vuln still fails the build — without it being -// permanently red over transitive advisories we cannot patch (nodemailer is -// pinned to 7 by next-auth's @auth/core peer dependency). +// permanently red over a transitive advisory we cannot patch without +// downgrading a framework (currently only postcss, bundled by Next.js). // // Usage (see .github/workflows/ci.yml): // npm audit --json --omit=dev > npm-audit.json || true @@ -12,18 +12,10 @@ import { readFileSync } from "node:fs"; -// Each entry is accepted because the vulnerable code path is unreachable given -// how server/email/index.ts uses nodemailer: sendEmail() accepts only the -// EmailPayload allowlist (from/to/subject/html/text/replyTo) and the transport -// sets only host/port/secure/auth{user,pass}. Re-triage if EmailPayload or the -// transport options ever widen. +// Each entry must be a triaged, no-fix-available advisory that is unreachable +// through our application surface. Shrink as fixes land. (The nodemailer cluster +// was removed once nodemailer 9 patched it — see package.json `overrides`.) const ALLOW = new Map([ - ["GHSA-c7w3-x93f-qmm8", "nodemailer envelope.size injection — we never set envelope"], - ["GHSA-vvjj-xcjg-gr5g", "nodemailer transport name CRLF — we never set name"], - ["GHSA-268h-hp4c-crq3", "nodemailer List-* header CRLF — we never set headers/list"], - ["GHSA-wqvq-jvpq-h66f", "nodemailer jsonTransport bypass — SMTP transport only"], - ["GHSA-r7g4-qg5f-qqm2", "nodemailer OAuth2 TLS — we use auth:{user,pass}, not OAuth2"], - ["GHSA-p6gq-j5cr-w38f", "nodemailer raw option file-read/SSRF — not in EmailPayload"], ["GHSA-qx2v-qp2m-jg93", "postcss stringify XSS — transitive via Next; no untrusted input"], ]);