Every Job That Can Write the Cache Can Publish
Eighty-four malicious @tanstack versions carried valid provenance: the signature named the right repository, the right workflow and the right commit, and every word of it was true

Ihor K
CEO
Eighty-four malicious @tanstack versions carried valid provenance: the signature named the right repository, the right workflow and the right commit, and every word of it was true

Ihor K
CEO
Most teams finished the migration from long-lived npm tokens to OIDC trusted publishing and signed provenance, then moved the ticket to done. The @tanstack compromise of 11 May 2026 is the argument for reopening it. The migration was right on its own terms: it turned a secret a person keeps into a credential a build holds for seconds. What it did not change is what gets to influence that build.
On 11 May 2026, between 19:20 and 19:26 UTC, an attacker published 84 malicious versions across 42 @tanstack packages and stole no publish token to do it. Those numbers and that window come from TanStack's own postmortem. The uncomfortable detail is that the supply-chain controls worked: every artifact carried signed build provenance naming the right repository, the right workflow and the right commit, and all of it was true. StepSecurity, which caught the publications within minutes, records the set as the first npm worm to ship malicious packages under valid SLSA Build Level 3 attestations. Provenance answers where a package was built. Whether the builder was running someone else's code that minute is a question the format was never given.
The chain is four defaults deep. A bundle-size workflow ran on pull_request_target, which executes with the base repository's permissions, and it checked out fork-authored content anyway. That gave fork code write access to the shared Actions cache, whose key on this repository was the string Linux-pnpm-store- followed by a hash of the lockfile, derivable in advance by anyone who can read the lockfile. The fork was renamed so it would not present as a fork, malicious code was force-pushed to the pull request head, and a 1.1 GB poisoned pnpm store was written under the predictable key. Then the attacker stopped. A later and entirely legitimate merge to main ran the release workflow, and the release workflow restored the cache.
The release job already held id-token: write for trusted publishing, and the runner mints that token in memory, inside its Runner.Worker process, at the moment a step asks for it. The payload located that process through /proc/*/cmdline, dumped its address space via /proc/<pid>/maps and /proc/<pid>/mem, and read the token out. Then the turn: the workflow's own publish step never ran. The tests failed, so the step was skipped, and the packages went out anyway, POSTed straight to registry.npmjs.org during cleanup. The credential was not stolen. It was issued, to exactly the identity the registry had been told to trust, inside a job that had already decided not to publish.
The general shape is easy to state and hard to inventory. A signature binds an artifact to a builder identity and says nothing about the integrity of what the builder consumed. The input set of a CI job is far wider than the repository the signature names: third-party actions pulled in by a mutable tag, base images, and a cache shared across refs instead of scoped to one. Tags and caches fail identically. actions/checkout@v6.0.2 is a name someone else can repoint, a cache key is a name someone else can fill, and TanStack's own follow-up says exactly that, putting a retargeted tag at the same blast radius as cache poisoning. So the perimeter that publishes a package is not the set of people holding a credential; it is the set of refs allowed to write anything the release job will later restore, and on this repository that set included every fork.
Consumer-side attestation verification would not have caught this one, and that is a bound on the control rather than an argument against it: a check that asks whether an artifact was built where it claims answers yes, correctly, for an artifact built exactly there by someone else's code. What closes this path is CI-side and dull. TanStack removed pull_request_target from its workflows outright, disabled the pnpm cache in the release pipeline, purged cache entries across the org, and pinned every action to a commit SHA. Granting id-token: write per job rather than per workflow belongs on the same list, because the publish step here was skipped and the permission standing in for it was not. Whatever comes next in this shape will verify too. A passing check is an accurate statement about the last step of a process whose earlier steps sit in .github/workflows, and the attestation format has no vocabulary for those steps — it was not designed to have one. The signature moved trust from a person to a builder and stopped there. In this incident everything downstream of that boundary held, and the packages still shipped.