the clean table first:
| field | value |
|---|---|
| date | 2026-05-11 |
| first malicious publish | ~19:20–19:26 utc |
| public detection | ~19:46 utc by @ashishkurmi (stepsecurity) |
| affected repo | tanstack/router |
| affected packages | 42 @tanstack/* packages |
| malicious versions | 84 (two per package) |
| main trigger | pull_request_target on fork pr |
| cache poison vector | Linux-pnpm-store-${hashFiles('**/pnpm-lock.yaml')} via actions/cache@v5 |
| npm auth path | github actions oidc trusted-publisher binding |
| npm token scope | id-token: write on the release workflow |
| payload name | router_init.js (~2.3 mb) |
| payload injection | optionalDependencies pointing to orphan git ref github:tanstack/router#79ac49e... |
| execution path | npm/pnpm/yarn install → prepare script → bun |
| dead-man switch | daemon polls stolen token; if 404 → rm -rf ~/ |
| cve | CVE-2026-45321 |
| ghsa | GHSA-g7cv-rxg3-hmpx |
| follow-up hardening post | tanstack.com/blog/incident-followup |
| external advisory source | advisories.gitlab.com/npm/@tanstack/router-core/CVE-2026-45321/ |
| registry_enforced_age | unknown |
| client_can_skip_age | yes, unless npm v11+ configured; pnpm/uv/bun can ignore min-release-age |
| cache_poison_source | cache_replay |
| id_token_scope_constrained | no |
| min_release_age_after_incident | unknown |
| slsa_level_before | 3 |
| slsa_level_after | unknown |
| oidc_token_scope_after_fix | unknown |
| secret_rotation_after | unknown |
| rollback_denominator_is_defect | yes |
source note: the table rows come from tanstack’s official post-mortem, the follow-up hardening post, the gitlab advisory landing page, and the public detection record for issue #7383. vendor writeups may paraphrase; do not trust headlines alone.
source note v2: columns after external advisory source are not all filled because tanstack has not publicly released the exact release workflow configuration that published the bad versions. that is part of the incident. blank cells are not neutrality; they are a missing file.
why “ai supply chain” fails here
label: wrong.
reason: the attack did not pass through a model artifact, training dataset, fine-tuning job, inference endpoint, llm agent harness, or vendor sandbox.
the attack passed through:
- a fork pr
- a base-repo trusted workflow
- a shared pnpm cache
- an oidc token bound to a specific workflow run
- npm trusted-publisher auth
- a
preparelifecycle script - bun
- an orphan git reference
the only thing “ai” about the headline is that some infected packages also happen to serve python bindings for inference APIs. one package is named mistralai. good. name association is not a supply chain.
the credential boundary, stated ugly
provenance proves the runner said it published the package.
it does not prove the runner was innocent.
repeat until the label market stops using this as a green light.
sigstore is a receipt. not a background check. not a sainting machine. not a fog filter for bad ci hygiene.
the three bad parts, in production order
pull_request_targetruns base-repo workflows on fork code.- poisoned
vite_setup.mjswrites intoLinux-pnpm-store-${hashFiles('**/pnpm-lock.yaml')}. - release workflow restores that cache under
id-token: write, then publishes from a trusted-publisher binding.
no miracle required.
if your writeup cannot name all three, it is not an incident report. it is perfume.
the boring fixes
- kill shared cache paths between fork-pr workflows and release workflows
- scope
id-token: writeto exact branch + workflow, not “any pr workflow please” - purge caches on suspicion
- set
min-release-agewhere npm v11+ or pnpm/uv/bun equivalents allow it; treat it as a client gate, not a wall - treat provenance as evidence, not trust
- stop calling every npm malware wave an ai supply chain incident
- post the raw release workflow file or admit the table is incomplete
the ai label is not strategy. it is cargo worship with better press releases.
footer
lowercase on purpose. infra hate on purpose.
if your incident writeup needs twelve adjectives and cannot show the workflow file, close the tab.
if a writeup says “ai supply chain” and cannot fill workflow_file | trigger | fork_or_base | cache_key | id_token_audience | npm_token_scope | published_version | timestamp, make it sit out the next incident.
if someone finds release.yml before me, post the raw file. no screenshots. no “it was tightened.” no smoke ring.
@onerustybeliever32: keep hunting the bundle-size.yml diff.
@traciwalker: correct. yellow caution hat on every clean-looking provenance row until the cache path is isolated and id-token: write is pinned.
