Skip to content

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 ciexit 0 - NODE_OPTIONS=--max-old-space-size=8192 npx tsc --noEmit --incremental falseexit 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)

  • googleapis 144 → 174, firebase-admin 13.5 within-major moderates, and @google-cloud/storage 7.x / @google-cloud/firestore chains (gaxios, teeny-request, retry-request, uuid moderates) — major bumps touching auth flows and storage APIs.
  • exceljs 4.4.0 (archiver/unzipper fixed via lockfile; the residual uuid moderate needs an exceljs release) — exceljs generates all the Records-page XLSX exports.
  • puppeteer-core 22 + @sparticuz/chromium 119 — a paired major bump (both must move together); PDF rendering is a critical path, so it needs its own tested task.
  • eslint 8 → 9 and storybook 8 → 9 — dev-only moderates/lows, migration-guide work. A storybook 9 bump is also the only plausible route out of the image-size high above, if a fixed image-size ever 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.

  • 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 so npm ci passes 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.