Upgrade the eight shipped dependencies carrying advisories

scan_sbom.py reported 50 advisory matches against shipped components, 16 of them
at or above HIGH. This clears all of them: the scan now reports 0 shipped
findings and the gate passes.

  pyjwt             2.12.1  -> 2.13.0
  idna              3.13    -> 3.19
  axios             1.13.2  -> 1.20.0   (29 advisories)
  postcss           8.5.6   -> 8.5.26
  dompurify         3.4.11  -> 3.4.14
  nanoid            3.3.11  -> 3.3.18
  follow-redirects  1.15.11 -> 1.16.0
  form-data         4.0.5   -> 4.0.6

Every one is an in-major bump inside a range the manifests already allowed, so
this is a lockfile refresh rather than a migration. The Python side was compiled
with --upgrade-package for each name so nothing else moved; the diff is two
version lines and their hashes.

nanoid is worth recording: it is transitive under postcss at ^3.3.11, and its
advisories are fixed on both the 3.x and 5.x branches. 3.3.18 is the fix that is
actually reachable.

The declared floors for axios and dompurify were raised to the versions in the
lockfile. They were ^1.6.0 and ^3.4.11, which still nominally admitted the
vulnerable range - npm resolves to the highest match so an install was never
going to pick one, but the manifest should state the minimum safe version rather
than rely on that.

axios 1.20 adds optional proxy-agent support, which pulls https-proxy-agent and
agent-base into the tree. Both are Node-only and neither reaches the browser
bundle; the build was checked for them. The main chunk grew 256kB to 271kB
(85.7kB to 90.5kB gzipped) from the rest of the axios delta.

The PyJWT entry in the allowlist was removed rather than kept. It stopped
matching the moment the upgrade landed, and an allowlist that accumulates
entries for advisories that no longer apply is one nobody rereads. The reasoning
is preserved as the worked example in docs/DEPENDENCY-SECURITY.md.

Verified: full suite 2161 passed, frontend 273 passed, build clean,
npm audit --omit=dev reports 0 vulnerabilities.
This commit is contained in:
cproudlock
2026-08-29 17:42:35 -04:00
parent 1cc2a6b782
commit b8fc2e9e33
6 changed files with 91 additions and 69 deletions

View File

@@ -61,13 +61,21 @@ because somebody would act on it.
accepted. An entry carries the advisory id, the reason, who accepted it, the
date, and what would invalidate the reasoning.
A good reason names the precondition that is absent. The first entry is the
worked example: a PyJWT algorithm-confusion advisory that needs a verifier
configured with both symmetric and asymmetric algorithms AND a raw JSON JWK
passed as the key. Neither is true here, and `invalidatedby` records the change
that would make it true.
A good reason names the precondition that is absent. The file has held exactly
one entry so far, and how it ended is the point.
That last field is the one that earns its keep. A reason stops being true when
A PyJWT advisory rated HIGH - algorithm confusion, forged HS256 tokens - needs a
verifier configured with both symmetric and asymmetric algorithms AND a raw JSON
JWK passed as the key. This application configures neither: there is no
`JWT_ALGORITHM` or `JWT_DECODE_ALGORITHMS` anywhere in the tree, so
flask-jwt-extended runs HS256-only against a string secret. That was written
down, with `invalidatedby` recording what would make it untrue - SSO issuing
tokens verified against a JWK.
Then PyJWT was upgraded, the advisory stopped matching, and the entry was
deleted. The allowlist is empty again, which is the state to aim for.
`invalidatedby` is the field that earns its keep. A reason stops being true when
the code that made it true changes, and nobody re-reads an allowlist looking for
that. Write down what would break it.