Skip to content

[Security 8/9] Supply-chain hygiene: CVE patches, audit ratchet, Dependabot - #251

Draft
amal66 wants to merge 15 commits into
open-legal-products:mainfrom
amal66:olp-pr/sec-supply-chain
Draft

[Security 8/9] Supply-chain hygiene: CVE patches, audit ratchet, Dependabot#251
amal66 wants to merge 15 commits into
open-legal-products:mainfrom
amal66:olp-pr/sec-supply-chain

Conversation

@amal66

@amal66 amal66 commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

[Security 8/9] Supply-chain hygiene — CVE patches, an audit ratchet, and Dependabot

Part of the split of #227 into single-topic PRs. Index: tracking comment on #227.

TL;DR

Patch known-vulnerable transitive dependencies via scoped overrides where an in-range fix exists (tmp ≥0.2.6, form-data 4.0.6), clear what npm audit fix can clear semver-compatibly (no --force), add a blocking CI audit gate for every directory that owns a lockfile (root, backend, frontend) with an explicit, documented GHSA allowlist — currently down to one entry — for advisories that have no in-range fix anywhere, and add Dependabot so the fix for a newly-published CVE arrives automatically as a bot PR. Lockfiles were regenerated on current main, never hand-merged.

Risk to user data

Severity: medium (systemic). Most of the code we ship is dependencies, and they go bad two ways: honest packages with disclosed CVEs, and outright compromised packages. Both run with our server's privileges — a vulnerable or malicious dependency in the request path can read documents, leak secrets, or execute code. Advisories fixed in-range here: tmp symlink traversal, form-data predictable multipart boundaries, ip-address SSRF-bypass (via express-rate-limit), nanoid infinite-loop DoS, undici moderates, plus highs in @xmldom/xmldom, protobufjs, ws, and others.

Flows affected

  • Build / dependency resolution (root, backend, and frontend — every directory with its own package-lock.json).
  • CI (.github/workflows/security.yml) — new blocking gate for all three trees, actions SHA-pinned per the [Security 19] security posture: SECURITY.md, CodeQL, secret scan, Scorecard #246 posture.
  • Frontend app code: adopting the stricter linter the upgraded floor pulls in (details below) — behavior-preserving restructurings, no feature change.

Attack precedent

  • event-stream (2018) — a popular package taken over and injected with wallet-stealing code. (writeup)
  • XZ Utils backdoor (2024) — a multi-year social-engineering supply-chain attack. (overview)
  • Vulnerable/compromised components is OWASP A06:2021.

Possible fixes, and what we chose

Option Verdict
npm audit fix --force Rejected — swaps in different major versions untested (here it would downgrade next to v9). Never in CI.
One-time npm audit fix and move on Clears today's advisories but the state rots the moment a new CVE lands. Necessary but not sufficient.
Global overrides forcing versions outside a consumer's declared range Rejected after review (thanks @willchen96) — an earlier revision forced sharp 0.35.x and postcss 8.5.x under a Next that declared sharp ^0.34.5 and pinned postcss 8.4.31 exactly. That ships combinations upstream never tested to make a scanner green. Overrides are only used where the forced version is inside the consumer's declared range (tmp, form-data).
Report-only (continue-on-error) frontend gate Rejected — it tolerates every future advisory silently, not just the known one.
Scoped in-range overrides + semver-safe audit fix + a blocking gate with a documented GHSA allowlist + Dependabot Chosen. Advisories with no in-range fix anywhere get an allowlist entry carrying the reason, the actual exposure, and an explicit REMOVE WHEN exit condition — printed by the gate on every run so they stay visible. Everything else blocks.
flowchart LR
    CVE["new advisory published<br/>(GitHub Advisory DB)"] --> Gate["CI audit gate goes red<br/>scripts/audit-gate.mjs"]
    CVE --> Bot["Dependabot opens a<br/>version-bump PR"]
    Bot --> CI["full CI runs on the bot PR:<br/>audit + tests + lint"]
    CI --> Merge["human reviews & merges"]
    Merge --> Green["gate green again<br/>(and allowlist entries retire)"]
Loading

Why the gate is a ratchet: scripts/audit-gate.mjs re-audits straight from each lockfile on every push (seconds, no install), so a newly published advisory fails the build even with zero code change — that fired three times during this PR's own lifetime: the July brace-expansion wave (absorbed in-range, its allowlist entry retired), the early-August ip-address/undici wave (absorbed in-range), and the mid-August nanoid wave (GHSA-28wg-ghj8-5hjv, GHSA-2v37-7h3g-55p8 — absorbed in-range: docx declares nanoid ^5.1.3, postcss declares ^3.3.16, so the patched 5.1.16/3.3.18 land without leaving any consumer's tested range). The gate fails closed: if npm audit itself errors (registry outage), the job fails rather than reporting a false green.

The allowlist is now down to 1 entry (scripts/audit-allowlist.json):

  • undici GHSA-4cwx-7wf7-3272 — dev-only, via miniflare's exact undici 7.28.0 pin. Remove when wrangler ships miniflare with undici ≥7.29.0.

The expiry mechanism has now demonstrated itself three times: the brace-expansion entry retired when the fix propagated in-range, the sharp entry retired when Next 16.3.0 began declaring sharp ^0.35.3, and both postcss entries retired when Next 16.3.0 moved its exact pin to postcss 8.5.23 — each removal is a commit that quotes the entry's own REMOVE WHEN condition coming true.

Adopting the linter the floor pulled in: regenerating the frontend lockfile moved eslint-plugin-react-hooks in-range from 7.0.1 to 7.1.1, whose new React-compiler-powered rules (set-state-in-effect, refs, preserve-manual-memoization, purity) flagged 70 errors across 37 files — and CI lints as a blocking gate. Suppressing the rules would discard exactly the signal the upgrade bought, so every error is fixed by restructuring (React-docs "adjust state during render" pattern for effect-based resets, ref access moved out of render, memo dependency arrays made verifiable), with a single documented eslint-disable where an effect genuinely coordinates committed DOM. All restructurings are behavior-preserving; the test suites and a production build back that claim.

Lockfile discipline: all lockfiles were regenerated with npm install --package-lock-only on the current base — never hand-edited or hand-merged. (Manually reconciling a lockfile can silently corrupt the dependency tree; regeneration from the manifest is the only safe path.)

Verified locally on the rebased branch: backend 550 tests + build pass; frontend lint 0 errors, 280 tests + coverage ratchet pass, and a full next build production build succeeds on the upgraded tree (Next 16.3.0 with the exact sharp/postcss versions it declares). All three audit gates exit 0.

What's in this PR

  • backend/package.json, frontend/package.json — scoped, in-range overrides only.
  • Both workspace package-lock.json files — regenerated on current main.
  • scripts/audit-gate.mjs + scripts/audit-allowlist.json — the blocking gate and its documented exceptions (now a single entry).
  • .github/workflows/security.yml — the audit gate for root, backend, and frontend, actions pinned to commit SHAs.
  • .github/dependabot.yml — grouped CVE-fix PRs + weekly dev-dep bumps for all three npm trees and GitHub Actions.
  • frontend/src/** — the eslint-plugin-react-hooks 7.1 adoption described above.

One repo setting must be flipped after merge (config files can't enable it): Settings → Advanced Security → "Dependabot security updates" → Enable (free for public repos). Without it this file still delivers weekly bumps, but the within-hours CVE-fix PRs are the point.

📚 Primer: package handling from first principles

New to lockfiles, semver, overrides, or audits? This section builds up every concept this PR relies on, from zero.

1. The dependency tree: you ship code you didn't choose

When frontend/package.json says "next": "^16.2.6", we chose Next. But Next has its own package.json declaring its dependencies (postcss, sharp, …), and those have theirs. The app ships this entire tree — roughly 1,500 packages of which we picked about 60. The ones we named are direct dependencies; everything they drag in are transitive dependencies. The asymmetry that drives this whole PR: we control our own declarations, but Next's declarations are Next's — written and tested by the Next team, frozen into each Next release.

2. Semver: what a version range promises

A version 8.4.31 is major.minor.patch. The semantic-versioning convention: patch = bug fixes only, minor = backward-compatible additions, major = may break you. Ranges express trust in that convention:

Declaration Meaning
"postcss": "8.4.31" exact pin — this version, nothing else
"postcss": "^8.4.31" caret — anything >=8.4.31 <9.0.0
"sharp": "^0.34.5" caret on a 0.x version — see below

The 0.x special rule. Below 1.0.0, semver says the API is unstable and anything may break, so by convention the minor position plays the role the major position normally plays. npm's caret encodes this with one rule: allow changes that don't touch the leftmost non-zero digit. So ^1.2.3 allows up to <2.0.0, but ^0.34.5 allows only >=0.34.5 <0.35.0 — sharp 0.34.5 → 0.35.3 looks like a tiny step, but it is a major upgrade in disguise (sharp 0.35 ships documented breaking changes). This single fact is why the earlier revision of this PR was wrong to force sharp 0.35 under a Next that declared ^0.34.5. (Next 16.3.0 has since moved its own declaration to sharp ^0.35.3 — the upgrade arrived the legitimate way, tested upstream, and it is what retired the sharp allowlist entry.)

3. package.json vs package-lock.json: the question and the answer

package.json declares ranges — many versions would satisfy it. package-lock.json records one concrete resolution: exactly which version of every package in the whole tree, with a hash of each, so every developer and CI run installs an identical tree. The lockfile must always be derivable from the manifests — which is why this PR regenerates lockfiles with npm install --package-lock-only and never hand-edits them (hand-merging a lockfile can silently corrupt the tree while looking fine in a diff).

4. Why a package pins exactly

Next 16.2.x declared "postcss": "8.4.31" — no range at all. That is the Next team saying: we compiled, tested, and shipped our CSS pipeline against this precise version; no substitutions. A declaration like this is a tested-compatibility statement. Nothing mechanically stops you from violating it — but everything downstream of violating it is your problem, not upstream's. (16.3.0 moved the pin to 8.5.23, which is how both postcss allowlist entries retired.)

5. Overrides: one tool, two opposite uses

npm's overrides field tells the resolver: "wherever anything in the tree asks for package X, hand it version Y instead — regardless of what was asked for."

  • Legitimate use (this PR's tmp and form-data entries): a patched version exists inside the range consumers already declared. Some link in the release chain is just slow to re-lock. The override accelerates delivery of a combination upstream already tests. Runtime risk ≈ zero.
  • Misuse (this PR's earlier sharp/postcss overrides, since removed): forcing a version outside a consumer's declared range. That fabricates a combination upstream never tested — for sharp, a native C image library (libvips bindings) running under next/image in production. It makes the scanner green by increasing runtime risk, which inverts the point of the scanner.

Same JSON key, opposite relationship to upstream's testing. The difference is not degree but kind.

6. Advisories and npm audit: set intersection, not code analysis

When a vulnerability is disclosed it gets a GHSA id in the GitHub Advisory Database, recording the package, the vulnerable version range, and a severity. npm audit intersects our lockfile with that database: "does any resolved version fall inside any advisory's vulnerable range?" No code runs, no install needed — which is why the gate costs seconds and why it can go red with zero code change: the database moved, not our tree. (It fired three times during this PR's own lifetime: the July brace-expansion wave, the early-August ip-address/undici wave, and the mid-August nanoid wave.)

This also explains the perverse incentive the earlier revision fell into: audit only compares versions. Force sharp to 0.35.3 and the intersection is empty — green — even though what actually happened is that runtime risk went up.

7. Why overrides or allowlists at all: the three honest decisions

A blocking gate forces every advisory to a decision, and there are only three honest ones: fix the version, accept the risk, or stop shipping. Nobody chooses the third, so the real machinery is:

  • Fix the version when the polite path can't do it yet → an in-range override (the fix exists; the release chain hasn't propagated it).
  • Accept the risk when no acceptable fix exists anywhere → an allowlist entry (as was true for sharp and postcss until Next 16.3.0, and remains true for undici-under-miniflare today).

Everything else people do — npm audit fix --force, report-only jobs, deleting the audit step, out-of-range overrides — is one of those two decisions made invisibly, where nobody can review, challenge, or undo it.

8. Why blocking + allowlist beats report-only

Scenario report-only job blocking + allowlist
Known advisory with no fix (undici-under-miniflare) tolerated tolerated — via one documented entry
Brand-new critical published tomorrow silently tolerated fails the build

Report-only doesn't tolerate one advisory; it tolerates every future advisory, silently. A gate that never fires is not a gate. The allowlist keeps the gate armed and makes each exception narrow (one GHSA id), visible (printed with its reason on every run, nagged when stale), and expiring (every entry carries a REMOVE WHEN condition naming the upstream event that retires it). Changing accepted risk requires a reviewed commit to scripts/audit-allowlist.json — a version-controlled risk register.

9. Vulnerable ≠ exploitable: reading the allowlist reasons

Each entry states the actual exposure, because a vulnerable version only matters if the vulnerable path is reachable: the undici flaw lives only under miniflare, wrangler's local dev simulator — a devDependency that is never part of the deployed artifact. (The retired postcss entries argued the same way: Next's pinned copy only processed this repo's own CSS at build time.) This reasoning is what a reviewer should challenge in the entry — that's why it's written down.

10. Why the production build is the load-bearing evidence

Declarations, lockfiles, and audit results are all static facts. The only thing that tests the tree as an integrated artifact is building it: next build compiles every route, type-checks, and exercises the CSS pipeline and image tooling against the real resolved versions. In review, an untested claim and a false claim are indistinguishable — so the claim "what Next declares actually works in this app" is backed by a green production build in CI, not asserted.

One-sentence summary: we stopped lying to the resolver about what versions Next can handle, told the truth in a documented, expiring risk register instead, and then proved the truthful configuration actually builds. Every mechanism here — semver ranges, lockfiles, audit, allowlist, build — is a different layer of making claims checkable by someone else.

Reading

npm audit docs · GitHub Advisory Database · OWASP A06:2021


Revision note: an earlier version of this description said the frontend gate was report-only, and an earlier revision of the branch replaced that with global sharp/postcss overrides without updating this text. Both are gone: the gate blocks everywhere, out-of-range overrides were removed in favor of the documented allowlist. The 2026-08-12 re-review round then rebased onto current main, absorbed the nanoid wave in-range, retired the sharp and both postcss allowlist entries (their exit conditions came true with Next 16.3.0), extended the gate and Dependabot to the repo-root lockfile, and adopted the eslint-plugin-react-hooks 7.1 rules the upgraded floor pulled in. This description matches the implementation as of that round.

@CLAassistant

CLAassistant commented Jul 23, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@amal66
amal66 force-pushed the olp-pr/sec-supply-chain branch from 53d00b8 to 6ff7033 Compare July 25, 2026 21:31
@amal66
amal66 requested a review from willchen96 July 26, 2026 14:10
@amal66
amal66 force-pushed the olp-pr/sec-supply-chain branch from 315ea3d to 0987b63 Compare August 3, 2026 01:57

@willchen96 willchen96 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think this should be merged as-is. The audit gate and Dependabot configuration are useful, and both audit jobs currently pass, but a few issues should be resolved first:

  • The frontend CI check is red. The coverage failure also existed on the PR's old base, so it may not have been introduced here, but it caused lint and the production build to be skipped. Current main is green; please rebase onto current main, regenerate the lockfiles, and rerun all checks.
  • The global sharp override resolves to 0.35.3, while Next 16.2.12 declares sharp compatibility as ^0.34.5. Sharp 0.35 includes documented breaking changes, so this forces Next outside its supported range without a successful production-build check demonstrating compatibility. Please remove the global override, upgrade to a Next version that supports Sharp 0.35, or explicitly demonstrate and justify compatibility. A temporary, documented GHSA allowlist is safer than silently overriding Next's declared range.
  • The PR still requires review.
  • The PR description says the frontend audit is report-only, but the current workflow makes both audit jobs blocking. Please update the description to match the implementation.

Once the branch is rebased, all CI—including the frontend production build—is green, and the Sharp compatibility issue is resolved, I would be comfortable merging the supply-chain automation.

amal66 added a commit to amal66/mike that referenced this pull request Aug 5, 2026
…ed allowlist entries

WHY THIS MATTERS
The previous fix for the sharp and postcss advisories used global npm
"overrides" that forced versions OUTSIDE the ranges their consumers
declare: next@16.2.x declares sharp ^0.34.5 (on a 0.x version, caret
pins the minor — ^0.34.5 means >=0.34.5 <0.35.0, so 0.35.x is excluded)
and pins postcss at exactly 8.4.31. Overriding those shipped Next
against a sharp 0.35.3 and postcss 8.5.25 it has never been tested
with — trading real runtime risk (native image pipeline under
next/image) for a green scanner. Review on PR open-legal-products#251 correctly rejected
this.

WHAT IS THE DIFFERENCE BETWEEN A GOOD AND A BAD OVERRIDE
An override is the right tool when a PATCHED version exists INSIDE the
consumer's declared range and the maintainer is merely slow to
re-lock (tmp ^0.2.6 and form-data ^4.0.6 here — both stay). When no
in-range fix exists anywhere, an override silently fabricates an
untested combination; the honest options are: upgrade the parent,
accept-and-disclose, or wait.

HOW IT WORKS NOW
The overrides are removed, so the lockfile resolves what upstream
actually tested: next's nested sharp 0.34.5 and postcss 8.4.31, with
the root tree independently resolving patched postcss 8.5.x in-range.
The three advisories that consequently reappear (sharp
GHSA-f88m-g3jw-g9cj, postcss GHSA-6g55-p6wh-862q /
GHSA-r28c-9q8g-f849) plus the new dev-only undici
GHSA-4cwx-7wf7-3272 (miniflare pins undici at exactly 7.28.0) are
recorded in scripts/audit-allowlist.json — each entry carries the
reason, the actual exposure, and an explicit REMOVE WHEN exit
condition, and the audit gate prints them on every run so they stay
visible. The gate stays blocking for both workspaces: unlike
report-only, a new advisory still fails the build; only these four
documented, tracked exceptions pass.

Verified: frontend lint 0 errors, 174 tests + coverage ratchet pass,
and a full production build succeeds against the exact sharp/postcss
versions Next declares.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@amal66
amal66 force-pushed the olp-pr/sec-supply-chain branch from 4295617 to f0e66d4 Compare August 5, 2026 15:30
@amal66

amal66 commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

@willchen96 Thanks for the review, addressed all points 1,2,4

  1. Rebased and regenerated lockfiles since the PR had drifted. All checks now run green.
  2. Removed the sharp override and global post css override so we just resolve the lockfile to what Next has declared and tested
  3. Your review is needed?
  4. Updated PR description to describe accurately how we block both workspaces with an allowlist.

@amal66
amal66 requested a review from willchen96 August 5, 2026 16:54
@willchen96

Copy link
Copy Markdown
Collaborator

Some issues:

Resolve the 60 frontend lint errors introduced by the dependency upgrade.

Remove the two obsolete PostCSS entries from the vulnerability allowlist.

Add the root npm dependencies to the audit workflow and Dependabot.

Update the PR description to reflect the current dependencies and results.

Ensure every CI check passes before merging.

@willchen96

Copy link
Copy Markdown
Collaborator

@amal66 Hi Amal could you resolve the remaining issues in my previous comment and rebase this PR? Then we can get this merged in.

@amal66
amal66 force-pushed the olp-pr/sec-supply-chain branch from 65a71dd to b1ee914 Compare August 13, 2026 03:04
amal66 added a commit to amal66/mike that referenced this pull request Aug 13, 2026
…ed allowlist entries

WHY THIS MATTERS
The previous fix for the sharp and postcss advisories used global npm
"overrides" that forced versions OUTSIDE the ranges their consumers
declare: next@16.2.x declares sharp ^0.34.5 (on a 0.x version, caret
pins the minor — ^0.34.5 means >=0.34.5 <0.35.0, so 0.35.x is excluded)
and pins postcss at exactly 8.4.31. Overriding those shipped Next
against a sharp 0.35.3 and postcss 8.5.25 it has never been tested
with — trading real runtime risk (native image pipeline under
next/image) for a green scanner. Review on PR open-legal-products#251 correctly rejected
this.

WHAT IS THE DIFFERENCE BETWEEN A GOOD AND A BAD OVERRIDE
An override is the right tool when a PATCHED version exists INSIDE the
consumer's declared range and the maintainer is merely slow to
re-lock (tmp ^0.2.6 and form-data ^4.0.6 here — both stay). When no
in-range fix exists anywhere, an override silently fabricates an
untested combination; the honest options are: upgrade the parent,
accept-and-disclose, or wait.

HOW IT WORKS NOW
The overrides are removed, so the lockfile resolves what upstream
actually tested: next's nested sharp 0.34.5 and postcss 8.4.31, with
the root tree independently resolving patched postcss 8.5.x in-range.
The three advisories that consequently reappear (sharp
GHSA-f88m-g3jw-g9cj, postcss GHSA-6g55-p6wh-862q /
GHSA-r28c-9q8g-f849) plus the new dev-only undici
GHSA-4cwx-7wf7-3272 (miniflare pins undici at exactly 7.28.0) are
recorded in scripts/audit-allowlist.json — each entry carries the
reason, the actual exposure, and an explicit REMOVE WHEN exit
condition, and the audit gate prints them on every run so they stay
visible. The gate stays blocking for both workspaces: unlike
report-only, a new advisory still fails the build; only these four
documented, tracked exceptions pass.

Verified: frontend lint 0 errors, 174 tests + coverage ratchet pass,
and a full production build succeeds against the exact sharp/postcss
versions Next declares.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@amal66

amal66 commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

@willchen96 Thanks — all points addressed, plus the rebase:

  1. Frontend lint errors — after the rebase onto current main the count was 70 (main grew since your review); all came from the in-range eslint-plugin-react-hooks 7.0.1 → 7.1.1 bump enabling the new React-compiler rules. All 70 are fixed by restructuring rather than suppression (mostly the React-docs "adjust state during render" pattern; ref access moved out of render; memo deps made verifiable). One documented eslint-disable remains where an effect genuinely coordinates committed DOM. Lint now reports 0 errors.
  2. Obsolete postcss allowlist entries — both removed. Their REMOVE WHEN condition ("Next bumps its postcss pin past 8.5.17") came true: Next 16.3.0 pins postcss 8.5.23. The sharp entry retired the same way (Next 16.3.0 declares sharp ^0.35.3), so the allowlist is down to a single entry — undici under miniflare, dev-only.
  3. Root npm dependencies — the audit-gate matrix and Dependabot now cover every directory that owns a lockfile: root, backend, frontend.
  4. PR description — rewritten to match the current tree and results.
  5. CI — rebased onto current main with lockfiles regenerated (not hand-merged). Verified locally: backend 550 tests + build; frontend 0 lint errors, 280 tests + coverage ratchet, full production next build; all three audit gates exit 0. One note: a fresh nanoid advisory wave (GHSA-28wg-ghj8-5hjv, GHSA-2v37-7h3g-55p8) landed mid-revision and was absorbed in-range — docx declares ^5.1.3 and postcss ^3.3.16, so patched 5.1.16/3.3.18 resolve without any override or allowlist entry.

Ready for re-review.


CI note on the rebased head: every check is green except "Fresh install vs upgraded deployment" (Schema drift), which is failing on current main as well (same whitespace-only formatting mismatch in the chats RPC between schema.sql and the migration path). It predates this PR — no SQL is touched here — so I've left it to be fixed where it was introduced rather than folding an unrelated schema fix into a supply-chain PR.

@amal66

amal66 commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

The red "Fresh install vs upgraded deployment" check mentioned above now has a fix up: #330 re-syncs schema.sql with the get_chats_overview migration text (formatting-only drift from 57d754c on main). Once that merges and this branch picks it up, every check here goes green.

@amal66
amal66 marked this pull request as draft August 16, 2026 22:50
amal66 and others added 12 commits August 21, 2026 10:45
Regenerated both lockfiles on current main with npm install
--package-lock-only + npm audit fix (semver-compatible only, no
--force). Backend passes npm audit --audit-level=high; frontend is
down to the pre-existing sharp/next chain (GHSA-f88m-g3jw-g9cj).
Adapted-from: open-legal-products#227 (58c8e9a, 8e89ab6)
…for both workspaces

Three advisories landed after this PR opened. postcss (GHSA-qx2v-qp2m-jg93,
GHSA-6g55-p6wh-862q, GHSA-r28c-9q8g-f849) and sharp (GHSA-f88m-g3jw-g9cj) are
fixed with non-breaking overrides — which also clears the next chain that
originally forced the frontend audit to report-only. brace-expansion
(GHSA-mh99-v99m-4gvg) has no non-breaking fix anywhere in the ecosystem (the
only patched release, 5.0.8, breaks every CJS consumer of minimatch <= 10.0.2),
so the audit becomes a blocking gate with an explicit allowlist
(scripts/audit-gate.mjs + audit-allowlist.json) instead of continue-on-error:
new advisories fail the build, documented-unfixable ones stay visible on every
run until Dependabot surfaces the upstream releases that retire them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… Dependabot version bumps

Two findings from an adversarial re-review: (1) when npm audit itself errors
(registry outage, ENOAUDIT) it prints a JSON error object that parsed as zero
advisories and passed the gate — now any report without a vulnerabilities key
fails closed (reproduced against an unreachable registry). (2) version updates
covered ALL deps as individual PRs, contradicting the file's comments; every
version bump now lands in one of two grouped weekly PRs (dev / production) per
workspace, and the comments describe the actual behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ce-expansion allowlist entry

WHY THIS MATTERS

The audit gate this PR introduces is deliberately blocking: any new
high/critical advisory against either lockfile fails CI instead of
scrolling past in a report-only log. The cost of that design is that the
gate goes stale on the PR's own timeline — advisories published after
the branch was written fail its checks even though the code didn't
change. That is the gate working as intended, and the remedy is the same
discipline the gate enforces on everyone else: absorb the wave with the
narrowest fix available, never by loosening the gate.

WHAT THE NEW ADVISORIES ARE

Since the last absorption (2026-07-25), 13 high advisories hit the
backend lockfile and 16 hit the frontend (all severity: high):

backend:
- @xmldom/xmldom <=0.8.12 — GHSA-2v35-w6hq-6mfw (recursion DoS),
  GHSA-f6ww-3ggp-fr8h, GHSA-x6wf-f3px-wcqx, GHSA-j759-j44w-7fr8
  (XML injection via unvalidated DocumentType / PI / comment
  serialization)
- fast-uri 3.0.0–3.1.3 — GHSA-v2hh-gcrm-f6hx, GHSA-4c8g-83qw-93j6
  (host confusion; ajv's URI resolver, on the request path via zod/ajv
  schema validation)
- fast-xml-builder <=1.1.6 — GHSA-5wm8-gmm8-39j9 (attribute-quote
  escape; sibling of our direct dep fast-xml-parser)
- protobufjs <=7.6.4 — GHSA-66ff-xgx4-vchm (code injection via bytes
  field defaults), GHSA-75px-5xx7-5xc7 (codegen gadget after prototype
  pollution), GHSA-jvwf-75h9-cwgg, GHSA-685m-2w69-288q,
  GHSA-wcpc-wj8m-hjx6 (three DoS vectors)
- ws 8.0.0–8.20.1 — GHSA-96hv-2xvq-fx4p (memory-exhaustion DoS from
  tiny fragments)

frontend:
- @xmldom/xmldom and ws — same four + one as backend
- next 16.0.0–16.2.10 — GHSA-6gpp-xcg3-4w24 (middleware bypass under
  Turbopack + single locale), GHSA-m99w-x7hq-7vfj (Server Actions DoS),
  GHSA-89xv-2m56-2m9x and GHSA-p9j2-gv94-2wf4 (two SSRF vectors)
- undici 7.x <7.28.0 — GHSA-vmh5-mc38-953g (TLS validation bypass via
  SOCKS5 ProxyAgent), GHSA-vxpw-j846-p89q (WebSocket fragment-count
  DoS), GHSA-hm92-r4w5-c3mj (cross-origin routing via proxy pool reuse)
- brace-expansion — GHSA-3jxr-9vmj-r5cp (exponential-time expansion)
- js-yaml 4.0.0–4.2.0 — GHSA-52cp-r559-cp3m (merge-key quadratic CPU)
- linkify-it <=5.0.1 — GHSA-22p9-wv53-3rq4, GHSA-v245-v573-v5vm
  (quadratic scan loops on attacker-controlled text)

HOW THE FIX WORKS

Every one of these has a fixed release inside the semver ranges our
package.json files (and their transitive parents) already declare, so
this wave needs the *cheapest* rung of the fix ladder: regenerate the
lockfiles with `npm audit fix --package-lock-only` — no dependency
declarations change, no new overrides, no allowlist growth. Backend
resolves xmldom 0.8.13, fast-uri 3.1.5, fast-xml-builder 1.3.0,
protobufjs 7.6.5, ws 8.21.1; frontend resolves next 16.2.12 (in-range
of ^16.2.6), undici 7.28.0, js-yaml 4.3.1, linkify-it 5.0.2, and
patched brace-expansion lines (1.1.18 / 2.1.4 / 5.0.9). The resolution
refresh also moves resend to 6.18.1, which drops its svix subtree (the
old uuid@10 consumer). backend/bun.lock is regenerated alongside
per repo convention (CI audits npm lockfiles only).

Overrides (the middle rung) stay untouched because no parent range
pins a vulnerable version anymore, and the allowlist (the last rung)
*shrinks*: upstream shipped patched brace-expansion releases on the old
major lines (1.1.17+ / 2.1.3+), so GHSA-mh99-v99m-4gvg — previously
"only fixed in 5.0.8, which breaks every CJS consumer" — is no longer
reported by either workspace. The gate has printed "remove it once no
workspace reports it" for exactly this moment; the entry is deleted and
the allowlist is empty again. This is the intended lifecycle: allowlist
entries are debt with a visible retirement condition, not permanent
exceptions.

Verified: audit gate exits 0 in both workspaces; backend vitest
375 passed / 14 skipped; frontend vitest 103 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ed allowlist entries

WHY THIS MATTERS
The previous fix for the sharp and postcss advisories used global npm
"overrides" that forced versions OUTSIDE the ranges their consumers
declare: next@16.2.x declares sharp ^0.34.5 (on a 0.x version, caret
pins the minor — ^0.34.5 means >=0.34.5 <0.35.0, so 0.35.x is excluded)
and pins postcss at exactly 8.4.31. Overriding those shipped Next
against a sharp 0.35.3 and postcss 8.5.25 it has never been tested
with — trading real runtime risk (native image pipeline under
next/image) for a green scanner. Review on PR open-legal-products#251 correctly rejected
this.

WHAT IS THE DIFFERENCE BETWEEN A GOOD AND A BAD OVERRIDE
An override is the right tool when a PATCHED version exists INSIDE the
consumer's declared range and the maintainer is merely slow to
re-lock (tmp ^0.2.6 and form-data ^4.0.6 here — both stay). When no
in-range fix exists anywhere, an override silently fabricates an
untested combination; the honest options are: upgrade the parent,
accept-and-disclose, or wait.

HOW IT WORKS NOW
The overrides are removed, so the lockfile resolves what upstream
actually tested: next's nested sharp 0.34.5 and postcss 8.4.31, with
the root tree independently resolving patched postcss 8.5.x in-range.
The three advisories that consequently reappear (sharp
GHSA-f88m-g3jw-g9cj, postcss GHSA-6g55-p6wh-862q /
GHSA-r28c-9q8g-f849) plus the new dev-only undici
GHSA-4cwx-7wf7-3272 (miniflare pins undici at exactly 7.28.0) are
recorded in scripts/audit-allowlist.json — each entry carries the
reason, the actual exposure, and an explicit REMOVE WHEN exit
condition, and the audit gate prints them on every run so they stay
visible. The gate stays blocking for both workspaces: unlike
report-only, a new advisory still fails the build; only these four
documented, tracked exceptions pass.

Verified: frontend lint 0 errors, 174 tests + coverage ratchet pass,
and a full production build succeeds against the exact sharp/postcss
versions Next declares.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WHY THIS MATTERS
A GitHub Actions tag like actions/checkout@v4 is mutable: whoever
controls (or compromises) the action's repository can move the tag to
different code, and every workflow using it runs that new code with
the workflow's permissions on the next trigger. This is the same
supply-chain shape this PR defends against in npm — except Actions
have direct access to the repo checkout and any secrets the job gets.

HOW IT WORKS
Referencing the 40-character commit SHA makes the reference immutable
— a SHA cannot be repointed. The human-readable version stays as a
trailing comment, and Dependabot's github-actions ecosystem (added in
this PR) bumps the SHA when new releases ship, so pinning does not
mean freezing.

This also aligns this new workflow with the repo-wide posture
established in open-legal-products#246, where codeql/gitleaks/scorecard workflows were
SHA-pinned for exactly this reason; OpenSSF Scorecard's
Pinned-Dependencies check verifies it stays that way.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WHY THIS MATTERS
Every GitHub Actions job receives a GITHUB_TOKEN. Unless a workflow
declares otherwise, that token inherits the repository default, which
can include write access to contents, checks, and pull requests. A
workflow that runs third-party code (actions, npm itself) with a
write-capable token is a privilege-escalation path: compromise
anything the job executes and you can push code or tamper with CI
state.

WHAT LEAST PRIVILEGE MEANS HERE
This job only checks out the repo and asks the npm registry about
advisories — it writes nothing. Declaring `permissions: contents:
read` shrinks the token to exactly that, so even a fully compromised
step cannot modify the repository.

HOW IT FITS THE REPO
Matches the posture of the other security workflows (codeql.yml,
gitleaks.yml, scorecard.yml all declare explicit permissions), and
OpenSSF Scorecard's Token-Permissions check — already running on this
repo — verifies workflows keep doing it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WHY THIS MATTERS

The committed frontend/package-lock.json was not the result of a clean
dependency resolution. During this PR's earlier lockfile surgery, Next's
declared optional dependency `sharp` (used by next/image for production
image optimization) lost its root-level installation: main resolved
`node_modules/sharp` at 0.34.5, but this branch's lockfile only carried
a copy nested under `node_modules/next/`. A lockfile that a fresh
resolve would not reproduce is a supply-chain smell: reviewers can no
longer trust that the lockfile equals "what npm would compute from
package.json", which is the whole property a lockfile exists to pin.

WHAT IS A "CLEAN RESOLUTION"?

npm treats package-lock.json as authoritative: `npm install` on top of
an existing lockfile keeps prior decisions, so drift introduced by
partial edits or interrupted installs persists silently. The way to
prove (or restore) integrity is to resolve from the manifest alone:

    rm package-lock.json
    npm install --package-lock-only

`--package-lock-only` computes the full dependency tree and writes the
lockfile without touching node_modules, so the result is purely "what
package.json implies today".

HOW THE FIX WORKS

This commit is exactly that regeneration. Because resolution picks the
newest versions the manifest's ranges allow, the tree moved forward:

  - next ^16.2.6 now resolves to 16.3.0, which declares sharp ^0.35.3.
    Root `node_modules/sharp` is therefore restored — at 0.35.3, past
    the 0.35.0 patch line of GHSA-f88m-g3jw-g9cj (libvips CVEs), so the
    advisory this PR had to allowlist for sharp 0.34.x no longer exists
    in the tree at all. miniflare's dev-only nested sharp stays at
    0.35.2, also past the patch line.
  - The security overrides this PR ships survive at patched versions
    everywhere in the tree: every `tmp` resolves to 0.2.7 (override
    `^0.2.6`) and every `form-data` to 4.0.6 (override `^4.0.6`).

Verified: `node ../scripts/audit-gate.mjs` passes in frontend (only the
already-allowlisted dev-only undici advisory reported) and backend
(zero advisories); `npm ci --dry-run` and a real `npm ci` succeed from
the new lockfile; `tsc --noEmit`, `vitest run` (174 tests), and
`next build` all pass on the regenerated tree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…came true

WHY THIS MATTERS

An audit allowlist is only trustworthy if every entry describes a real,
currently-reported advisory and names the condition under which it must
be deleted. The GHSA-f88m-g3jw-g9cj entry had drifted from reality on
both counts: it claimed the exposure was "next@16.2.x declares sharp
^0.34.5 ... no in-range fix exists", but after the previous commit's
clean lockfile regeneration there is no vulnerable sharp anywhere in
the tree. A stale entry is worse than none — if the advisory ever
resurfaced (e.g. a future dependency pinning an old sharp), the gate
would wave it through under a justification written for a different
world.

WHAT IS AN ALLOWLIST "REMOVE WHEN" CLAUSE?

scripts/audit-gate.mjs keeps `npm audit` blocking: high/critical
advisories fail CI unless their GHSA id sits in
scripts/audit-allowlist.json with a documented reason. Each reason ends
with a REMOVE WHEN clause — a falsifiable exit condition — so entries
are debts with a maturity date, not permanent exceptions. The gate
reinforces this by printing on every run:

    note: allowlist entry GHSA-... not reported in this workspace —
    remove it once no workspace reports it

HOW THE FIX WORKS

The entry's own exit condition — "REMOVE WHEN: Next declares sharp
^0.35.x" — has literally occurred: next 16.3.0 (in-range for this
manifest's ^16.2.6) declares sharp ^0.35.3, so the clean lockfile now
resolves root sharp at 0.35.3 and miniflare's dev-only nested copy at
0.35.2. The advisory's vulnerable range is < 0.35.0 (patched in
0.35.0), so neither instance is affected, and `npm audit --json` no
longer reports GHSA-f88m-g3jw-g9cj in the frontend or the backend
workspace. Per the gate's own rule, the entry is deleted rather than
rewritten — there is no accurate reason one could write for allowlisting
an advisory that no dependency triggers.

Verified: `node ../scripts/audit-gate.mjs` passes in both workspaces
after the removal (frontend: only the allowlisted dev-only undici
advisory; backend: zero advisories).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d wave in-range

Rebasing onto current main replayed the branch's manifest changes, but a
lockfile cannot be hand-merged — the rebase left the pre-rebase
resolutions in place. Both lockfiles are regenerated here from the
manifests with `npm install --package-lock-only`, so the tip is once
again a pure resolution of what the manifests declare on the new base.

The fresh audit then caught the next advisory wave — the third during
this PR's lifetime, which is the ratchet doing its job: two new high
advisories against nanoid (GHSA-28wg-ghj8-5hjv, GHSA-2v37-7h3g-55p8,
infinite-loop DoS in non-secure/custom generators). Both fixes are
in-range — docx declares nanoid ^5.1.3 and postcss declares ^3.3.16, so
`npm update nanoid` moves the resolutions to patched 5.1.16 / 3.3.18
without leaving any consumer's tested range. No override and no
allowlist entry needed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
amal66 and others added 3 commits August 21, 2026 10:49
…dition came true

Every allowlist entry carries a REMOVE WHEN condition naming the
upstream event that retires it. For both postcss advisories
(GHSA-6g55-p6wh-862q, GHSA-r28c-9q8g-f849) that condition was "Next
bumps its postcss pin past 8.5.17" — and Next 16.3.0, now resolved on
current main, pins postcss 8.5.23. The gate has been printing these
entries as unreported in every workspace since the rebase; the risk
register shrinks back to one entry (undici via miniflare, dev-only).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pendabot

The repo root grew its own package.json/package-lock.json (Playwright
e2e tooling), which means a third resolved dependency tree that neither
the audit gate nor Dependabot was watching — an advisory against a root
dev dependency would have landed silently. The gate matrix and the
Dependabot npm ecosystems now enumerate every directory that owns a
lockfile: root, backend, frontend. Same blocking semantics, same shared
allowlist, same grouped-PR cadence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…or pulled in

Regenerating the frontend lockfile moved eslint-plugin-react-hooks
in-range from 7.0.1 to 7.1.1, whose new compiler-powered rules flagged 86
existing errors across 34 files: set-state-in-effect (49), refs (21),
preserve-manual-memoization (15), purity (1). Since CI lints as a
blocking gate, the upgraded floor cannot land without either fixing the
patterns or suppressing the rules — and suppression would discard exactly
the signal the upgrade bought.

Every error is fixed by restructuring, not silencing (one targeted
disable survives, in TRChatPanel, where an effect genuinely coordinates
committed DOM — with a comment saying why):

- setState-in-effect resets/derivations now use the React-docs
  "adjust state during render" pattern (previous-value guard) or become
  plain derived values, so state lands before paint instead of one
  commit after — same converged state, minus a potential flash frame.
- Render-time ref reads/writes moved into effects, handlers, or plain
  render-scope consts, preserving each ref's mount-snapshot semantics.
- Manual useMemo/useCallback declarations were reordered or had
  property reads hoisted so the compiler can verify their dependency
  arrays; no dependency semantics changed.
- The one purity hit (Date.now/new Date during render) moved into the
  event handler that actually creates the optimistic row.

Two behavioural notes, both consequences of the pattern rather than
incidental: render-phase adjustment converges in the same render, so
parents observing DocTable's callbacks see one fewer intermediate commit
carrying stale breadcrumbs; and UserProfileContext's `loading` now starts
false for signed-out visitors, since they have no profile to fetch.

Verified: eslint 0 errors, tsc clean, 480 tests + coverage ratchet green,
and a full production next build passes on the upgraded tree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@amal66
amal66 force-pushed the olp-pr/sec-supply-chain branch from b1ee914 to 475577d Compare August 21, 2026 18:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants