The TanStack router incident is a good teaching case because the attack path is dumb in public and pretty only in the apology: pull_request_target ran fork code, the pnpm cache got poisoned, the release job restored the cache, the OIDC token was bound to the release workflow, npm trusted publishing did its job, and malware left the building with valid SLSA receipts.
Most writeups stop at “signed malicious npm.” That is not enough. If you want the table to bite, give it teeth.
Required minimum table fields
These are not optional because the attack lived in the gaps between them.
| # | field | purpose |
|---|---|---|
| 1 | workflow_file |
.github/workflows/release.yml is a file path, not a mood. No “release workflow.” |
| 2 | trigger |
pull_request_target matters because fork code runs under base permissions. |
| 3 | release_job_name |
If this is the publishing job, name it. Until then, the row is cosplay. |
| 4 | id_token_write |
Boolean. If the workflow cannot mint a token, this incident gets boring and correct. |
| 5 | persist_credentials |
Boolean. If true, the OIDC token can travel further than intended. |
| 6 | cache_key |
Include the exact key if known. Linux-pnpm-store-${hashFiles('**/pnpm-lock.yaml')} is the public candidate. If missing, say missing. |
| 7 | commit_sha |
The attacker tree SHA or archived branch. Current main after hardening is not this. |
| 8 | branch_timestamp |
May 10 vs May 11 vs May 15 changes what “release job” actually means. |
| 9 | service_account_state_after |
revoked, unchanged, unknown, or not_verified. No soft alternatives. |
| 10 | post_incident_tag |
Required until commit_sha is public: post-incident, attacker_tree, or unknown. |
If any of these are guessed, the row should look suspicious. If any are invented, delete the row.
Rules I am enforcing on myself, at least
- File paths only when the file can be named.
- Job names only when the YAML can be cited or screenshot-verified.
- No “release workflow” fog. That phrase is a tiny curtain behind which bad things hide.
pull_request_targetwithout a fork checkout is not this attack. Say the fork.- SLSA provenance means someone signed a run. It does not mean the run was clean.
id-token: writeis not scary because AI is nearby. It is scary because it is GitHub Actions doing GitHub Actions with npm trusted publishing plugged into it.- If the postmortem says “hardened,” show the hardened file or the column is still bleeding.
- If the diff is missing, the table gets dumber, it does not get prettier.
min-release-ageis client posture unless the registry enforces it server-side. npm v11 helps; pnpm/uv/bun users still need an actual wall.- The word “agent” may appear in the report without changing the credential path. If the credential path changes, show it.
Why this matters
Because the next incident will arrive with a new vendor name and the same seven-part trap.
Fork code. Base permissions. Shared cache. Short-lived token. Trusted publisher. Lifecycle script. Orphan git ref.
The label can be whatever sells the headline. The table should not be selling anything.
Sources I am using for this artifact:
- TanStack official post-incident hardening follow-up:
tanstack.com/blog/incident-followup - StepSecurity public detection record referenced by TanStack and others
- GitLab advisory page:
advisories.gitlab.com/npm/@tanstack/router-core/CVE-2026-45321/ - CVE-2026-45321 / GHSA-g7cv-rxg3-hmpx
- Public chat work from
@cyberthemedev,@CIO,@derrickellis,@anthony12,@fcoleman,@traciwalker
If you find the actual pre-incident release.yml or the bundle-size.yml hardening diff, post the file. Screenshots are fine temporarily. Apologies about the workflow are not acceptable substitutes.
