T-201 · Dependabot triage — clear all critical/high npm vulnerabilities via targeted upgrades¶
Why this task exists¶
GitHub reported 143 open Dependabot vulnerabilities on the default branch (2 critical,
75 high, 58 moderate, 8 low). The owner asked for a triage of the critical/high alerts:
identify production vs dev-only exposure, apply targeted upgrades (explicitly avoiding a
blanket npm audit fix --force, which jumps major versions and breaks things), gate with the
AGENTS.md fresh-npm ci + unfiltered tsc type-check, and merge without deploying.
What the alerts were (triage, 2026-08-06)¶
Local npm audit against the three lockfiles (root, services/bank-login-service,
services/workspace-billing-service) reads the same GitHub Advisory Database Dependabot uses
and reproduced the picture: root alone showed 2 critical / 79 high / 51 moderate / 90 low.
The two criticals — both in production dependencies:
- next-auth 4.24.14 — three Auth.js advisories (malformed-JWE crash in getToken(),
Unicode email-normalizer bypass, OAuth state/nonce/PKCE check-cookies not verified).
In plain words: the library that signs users in had known ways to be crashed or tricked.
- websocket-driver 0.7.4 — resource-limit bypass + message corruption; pulled in by
Firebase's realtime-database client, so it ships with the app server.
High-severity, production: next 15.5.19 (8 advisories: SSRF in Server Actions and
rewrites, response-cache confusion, image-optimizer DoS, server-function disclosure),
sharp 0.34.5 (inherited libvips CVEs; sharp processes every uploaded receipt image),
adm-zip 0.5.17 (crafted ZIP → 4GB allocation — and we feed it untrusted DMARC report
ZIPs from email in lib/dmarc/extractAttachments.ts, so this one was genuinely exposed),
mailparser 3.9.8 (exact-pinned vulnerable nodemailer 8.0.5 + linkify-it 5.0.0 —
mailparser parses inbound billing emails), plus semver-fixable transitives (form-data
CRLF injection under the google-cloud chain, ip-address/socks under puppeteer's proxy
chain, brace-expansion, postcss, js-yaml, fast-uri, ws, OpenTelemetry jaeger
propagator under inngest, archiver/unzipper under exceljs).
High-severity, dev-only: the eslint 8 / storybook 8 / webpack / vite-vitest / ts-prune / bundle-analyzer chains — build- and test-time tools that never run in production.
What was done (all semver-targeted; no --force, no major jumps)¶
Direct-dependency bumps in package.json:
- next ^15.5.9 → ^15.5.21 (patch series; fixes all 8 Next advisories)
- next-auth ^4.24.13 → ^4.24.15 (patch; fixes all 3 critical Auth.js advisories)
- mailparser ^3.9.8 → ^3.9.14 (patch; brings nodemailer 9.0.3 + linkify-it 5.0.2)
- sharp ^0.34.5 → ^0.35.3 (fixes the libvips CVE bundle) — plus an overrides
entry pinning sharp tree-wide, because next optionally pins ^0.34.x for its own
image optimizer; without the override a second vulnerable copy would nest under next
and be the one actually used on a self-hosted/NAS deployment. On Vercel, image
optimization is platform-side, so the override matters only for the NAS fallback.
- adm-zip ^0.5.17 → ^0.6.0 (fixes the crafted-ZIP allocation bug; the code uses
only the stable new AdmZip(buf) / getEntries() API, unchanged in 0.6)
Lockfile-only transitive updates (npm update + plain npm audit fix, both
semver-range-respecting): websocket-driver → 0.7.5 (the second critical; Firebase's
faye-websocket accepts >=0.5.1), ip-address → 10.4.0, form-data → 2.5.6,
brace-expansion, postcss, js-yaml, fast-uri, ws, @opentelemetry/propagator-jaeger,
and the archiver/unzipper/glob/rimraf + eslint/storybook chains.
The vite copy nested under vitest needed an override, vite: ~8.1.5 (vulnerable
range was >=8.0.0 <=8.0.15; the tree had 8.0.7). npm update will not touch a nested
lockfile entry, and the first attempt — dropping the nested entry so npm re-resolved it —
let it fall back to vite 6.4.3, which cleared the advisory but broke vitest at
startup (vitest 4's CJS config loader can't load a config under vite 6). The override
pins it forward instead of back. It is deliberately ~8.1.5, not ^8.1.5: ^ resolved
to 8.2.x, which swaps in the rolldown bundler — a larger change than a security patch
should carry. Only vitest depends on vite, so the override's blast radius is the test
runner alone.
Service lockfiles: npm audit fix (no force) in services/bank-login-service (now 0
vulnerabilities of any severity) and services/workspace-billing-service (0 critical/high).
Outcome¶
| Manifest | Before (crit/high) | After (crit/high) | Residual |
|---|---|---|---|
root package-lock.json |
2 / 79 | 0 / 2 (both dev-only, no upstream fix) | 13 moderate + 5 low |
root, production tree only (--omit=dev) |
2 / 50 | 0 / 0 | 11 moderate |
services/bank-login-service |
0 / 9 | 0 / 0 | none at all |
services/workspace-billing-service |
0 / 6 | 0 / 0 | 7 moderate |
Every critical and high in the production tree is cleared. The two highs still counted
against the root lockfile are image-size (ICNS/JXL/HEIF parser infinite-loop DoS,
GHSA-w3rx-r6r6-pgpr + GHSA-5p2g-fcmc-qvqq) and the @storybook/nextjs entry that only
inherits it. Both are dev-only — confirmed absent from the --omit=dev tree — and
npm audit reports no fix available at any version (<=2.0.2 is the whole release
history). They surfaced mid-session when the advisory database published them; they are
not a regression from this work and nothing in this repo can currently resolve them.
Verification (AGENTS.md merge gate)¶
Run on the final lockfile, after rm -rf node_modules:
- npm ci → exit 0
- NODE_OPTIONS=--max-old-space-size=8192 npx tsc --noEmit --incremental false → exit 0,
zero lines of output (unfiltered, non-incremental, fresh deps — the gate exactly as
AGENTS.md specifies it).
The vitest suite could not be used as a signal, and this is pre-existing. It fails at
startup with Cannot find native binding (rolldown's platform binary — the known npm
optional-dependency bug, npm/cli#4828) on this machine, which runs an x64 Node build on
darwin. This was verified by stashing every change in this task, reinstalling from the
unmodified HEAD lockfile, and running the suite: it fails there identically. So the
failure is the local toolchain, not this task's upgrades — but it does mean the runtime
behaviour of the sharp and adm-zip bumps is unverified by tests here, and rests on
the API review below instead.
Why the two majors are safe for how this repo uses them¶
Both were checked against upstream changelogs rather than assumed:
- sharp 0.34 → 0.35 — breaking changes are: Node.js >=20.9.0 required, the install
script removed, failOnError / paletteBitDepth / deprecated sharpen properties removed,
format.jp2k renamed jp2, a new limitInputChannels default of 5, and lossy-AVIF quality
metrics. None of the removed options appear anywhere in this repo (grepped). Every call site
uses trim / rotate / extract / metadata / raw / jpeg / png / composite /
resize, none of which changed signature or behaviour. Receipt images are RGB/RGBA, well
under the new 5-channel limit. Node requirement is met on both targets: the NAS Dockerfile
builds and runs on node:22-slim, and the lockfile carries all 16 linux prebuilt
@img/sharp-* binaries so a clean-room npm ci on Vercel/NAS resolves a real binary.
- adm-zip 0.5 → 0.6 — the only breaking change is to extractEntryTo(..., maintainEntryPath=false),
which this repo never calls. lib/dmarc/extractAttachments.ts uses only new AdmZip(buf),
getEntries(), entry.isDirectory and entry.entryName, all unchanged.
Residual risk — deliberately NOT fixed here (each needs a major-version project)¶
googleapis144 → 174,firebase-admin13.5 within-major moderates, and@google-cloud/storage7.x /@google-cloud/firestorechains (gaxios,teeny-request,retry-request,uuidmoderates) — major bumps touching auth flows and storage APIs.exceljs4.4.0 (archiver/unzipper fixed via lockfile; the residualuuidmoderate needs an exceljs release) — exceljs generates all the Records-page XLSX exports.puppeteer-core22 +@sparticuz/chromium119 — a paired major bump (both must move together); PDF rendering is a critical path, so it needs its own tested task.eslint8 → 9 andstorybook8 → 9 — dev-only moderates/lows, migration-guide work. A storybook 9 bump is also the only plausible route out of theimage-sizehigh above, if a fixedimage-sizeever ships.- The
@ant-design/charts→@antv/g6→ old-webpack chain — no upstream fix exists; browser-bundle-only code, not reachable as a server attack surface. image-size(via@storybook/nextjs) — no fixed version exists upstream; dev-only.
One thing for the owner to confirm before the next production deploy: sharp 0.35
requires Node ≥ 20.9. The NAS path is fine (node:22-slim), and the repo pins no
engines.node / .nvmrc, so Vercel uses its project-level Node setting — worth a glance at
Vercel → eop → Settings → Node.js Version that it is not still on 18. Nothing was changed
here to force it, deliberately: an engines pin would be a scope expansion on a security task.
Dependabot's remaining count on GitHub should drop to roughly the moderate/low residual once it re-scans the merged lockfiles.
Decision log¶
2026-08-06 — task opened, approach recorded.
Attestation: AGENTS.md read; board checked by scope — no existing task covers dependency
vulnerability triage. Source: Dependabot Assistant · local session
1fafa0d3-166b-4efe-8df1-aff416391d5e
Proposed: the owner (session brief: triage crit/high, prod-vs-dev split, targeted upgrades,
no blanket npm audit fix --force, AGENTS.md type gate, merge without deploy).
Approved: the owner, same brief. Owner's words (verbatim, 2026-08-06): "Triage the
critical/high alerts: identify which are in production dependencies vs dev-only, propose
targeted upgrades (avoid a blanket npm audit fix --force), and run the AGENTS.md type-check
gate (fresh npm ci + NODE_OPTIONS=--max-old-space-size=8192 npx tsc --noEmit --incremental
false) before any merge. Commit to main per the current branch policy; do not deploy."
Rationale for the two non-obvious choices: (1) the tree-wide sharp override (see "What was
done") — chosen over letting a second vulnerable sharp nest under next, because the NAS
fallback deployment would execute that nested copy; (2) the vite: ~8.1.5 override — chosen
over re-resolving the nested entry, which fell back to vite 6 and broke the test runner.
Firestore: no structure changes — this task touches no Firestore documents at all; reads
and writes nothing.
2026-08-06 — work complete and gated; NOT yet merged.
Source: Dependabot Assistant · local session 1fafa0d3-166b-4efe-8df1-aff416391d5e
Status stays doing, not done, because done means merged to main and this is not
merged yet. AGENTS.md requires the merge to happen as a pull request merged on GitHub,
never a local git merge into main — and this session has neither the gh CLI nor a
GitHub token, so it can push the branch but cannot open or merge the PR. The branch is
pushed and gate-green; the owner (or any session with GitHub access) opens the PR from
claude/laughing-hamilton-8295db and merges it, then this task flips to done. Per the
owner's instruction nothing was deployed, and under the current 🟡🟡 signal the merge itself
triggers no deploy.
What landed vs. the plan: the plan was "clear every critical/high." Production tree:
achieved — 2 critical and 50 high → 0 and 0. Whole root lockfile including dev: 2/79 →
0/2, the two residual highs being image-size and its @storybook/nextjs parent, which
have no fixed version upstream and never ship to production. Both service lockfiles are
clear of critical/high; bank-login-service is clear of everything. No --force was used
and no major version was jumped except the two (sharp, adm-zip) whose changelogs were
read against this repo's actual call sites first.
How it was verified: the AGENTS.md gate on a fresh npm ci with node_modules deleted —
npm ci exit 0, unfiltered non-incremental tsc --noEmit exit 0 with zero output. The vitest
suite is broken on this machine independently of this task (proved by reproducing the failure
on an unmodified HEAD install); see "Verification" above for what that leaves unverified.
Blast radius for other agents: this touches package.json + all three lockfiles, so any
agent with an in-flight branch will hit a lockfile conflict — rebase rather than merge, and
re-run npm ci. Behavioural surfaces worth knowing about: receipt/WOPC image processing
(sharp 0.34 → 0.35), DMARC ZIP extraction (adm-zip 0.6), inbound billing-email parsing
(mailparser 3.9.14, which moves nodemailer to 9.0.3), auth (next-auth 4.24.15), and the
framework itself (next 15.5.22). Anyone deploying should first confirm Vercel's Node version
is ≥ 20.9 per the note above.
Related commits (append-only)¶
0c50f15c— T-201: targeted dependency upgrades clearing all production critical/high Dependabot alerts (package.json + 3 lockfiles, task board, decision index)82808c77— docs(T-201): add Dependabot Assistant to the session ledger- (follow-up) — rebase onto current
main+ lockfile re-synced sonpm cipasses under npm 10 as well as npm 11; see the note below
Post-rebase note — a lockfile that only one npm accepted¶
2026-08-10. Rebasing this branch onto current main surfaced a defect this work had introduced
and the original gate had missed: npm ci failed on the branch with ~30 Missing: … from lock
file errors (esbuild, yaml, and the whole @esbuild/* platform set).
The cause was not the rebase — the lockfile was byte-identical before and after. It was the npm version. The machine's Node 22.11 carries a manually-upgraded npm 11.14.1, which wrote this lockfile; the Node 22.12 installed to run Storybook carries its bundled npm 10.9.0, which rejected it. The lockfile surgery in this task (dropping nested entries so npm would re-resolve them) produced a tree that only npm 11 considered complete.
That mattered: npm ci is what a clean-room build runs, so a lockfile only npm 11 accepts is a
build that fails wherever npm 10 is in play. Confirmed it was this branch and not pre-existing —
unmodified main installs cleanly under npm 10.9.0 (exit 0) while this branch did not.
Fixed by re-resolving the lockfile with the older npm (10.9.0), whose output npm 11 also accepts.
Now verified both ways: npm ci exit 0 under npm 10.9.0 and under npm 11.14.1.
Lesson for the next agent: hand-editing package-lock.json to force re-resolution works, but the
result is only guaranteed against the npm that wrote it. Re-run npm ci with the oldest npm the
project might meet before calling the gate green — a passing gate on one npm is not a passing gate.