Summary: For six minutes on May 11, 2026 — 19:20 through 19:26 UTC — npm accepted 84 compromised releases across 42 TanStack packages. Yet once an intruder can goad a runner into minting a publish token, the line disappears: whoever can start a workflow can also plant malware in every install downstream. The registry had no reason to object: these were not forgeries but genuine artifacts of TanStack’s own Router/Start release workflow, signed in with a valid OIDC identity through GitHub Actions trusted publishing.
For six minutes on May 11, 2026 — 19:20 through 19:26 UTC — npm accepted 84 compromised releases across 42 TanStack packages. The registry had no reason to object: these were not forgeries but genuine artifacts of TanStack’s own Router/Start release workflow, signed in with a valid OIDC identity through GitHub Actions trusted publishing. No npm token changed hands, and the legitimate publish pipeline itself was never breached. A postmortem issued after a three-day security sweep reconstructs how three run-of-the-mill misconfigurations, chained together, handed an attacker full command of the release pipeline behind a heavily downloaded open-source project.
This is what carries the story beyond a single CVE. Trusted publishing exists precisely as the safe replacement for long-lived npm tokens. Yet once an intruder can goad a runner into minting a publish token, the line disappears: whoever can start a workflow can also plant malware in every install downstream.
How Far It Reached: One Monorepo, 42 Packages, 84 Versions
Everything affected lived inside the Router/Start monorepo. The list takes in @tanstack/history at 1.161.9 and 1.161.12 together with 41 companions — react-router, vue-router, solid-start, router-core, and router-generator among them — each package carrying two poisoned releases that landed about six minutes apart. Elsewhere the record was clean: @tanstack/query, table, form, virtual, and store, plus every other TanStack repository, came through untouched. Delivery ran through a counterfeit optional dependency, @tanstack/setup, locked to orphan commit 79ac49ee, so any install done with npm, pnpm, or yarn would fetch and run an obfuscated script of roughly 2.3 MB answering to the name router_init.js.
The Chain of Attack: Three Flaws, Each One Required
The opening move was a textbook pwn request. bundle-size.yml fired on the pull_request_target trigger and checked out a fork’s PR merge ref (refs/pull/7378/merge) ahead of its build, putting attacker-authored code to work inside a context that held repository permissions — and the approval gate normally applied to first-time contributors never entered the picture. The groundwork predated the event by a day: on May 10, an account doing business as zblgg forked the project under the name “configuration” to slip past fork searches, then committed a payload of roughly 30,000 lines under the borrowed identity “claude” with the address claude@users.noreply.github.com — unconnected to Anthropic’s Claude — and tagged it [skip ci].
The second move poisoned the Actions cache. The planted files sat in the pnpm store under exactly the cache key release.yml would compute later on. Because PR runs and pushes to main drew on a single shared cache scope, and nothing checked permissions before the post-job cache save, a poisoned 1.1 GB cache entry settled in at 11:29 UTC on May 11. The attacker then wound the PR back to a no-op touching zero files, closed it, and removed the branch. On paper the PR was spotless; the contamination lived on in the cache.
The third move went after the OIDC token. At 19:15 that evening, the release workflow ran again and pulled the poisoned cache back in. Binary code planted by the attacker swept /proc command lines until it found the Actions Runner.Worker process, drained its memory, and recovered the OIDC token that had been minted on demand (the workflow legitimately carried id-token: write for npm trusted publishing), then sent it straight to registry.npmjs.org by POST — the Publish Packages step never ran at all. The method was taken nearly word for word from the March 2025 tj-actions/changed-files compromise.
The Payload’s Three Jobs: Steal, Exfiltrate, Multiply
Router_init.js worked to a three-part brief. It swept up credentials: AWS IMDS and Secrets Manager, GCP metadata, Kubernetes service-account tokens, Vault tokens, the contents of ~/.npmrc, GitHub tokens, and SSH private keys. It moved the haul out through Session messenger endpoints such as filev2.getsession.org under end-to-end encryption, leaving no conventional C2 server for anyone to sinkhole — blocking IPs and domains is all that remains for network defenders. And it spread itself: the script asked npm which other packages the compromised maintainer owned and republished every one of them carrying the same injection.
Finding It and Stopping It: An Outsider Beat Everyone to the Alarm
The alert that counted came from outside the project. At 19:46:46 UTC, ashishkurmi of StepSecurity filed issue #7383 on the TanStack repository, some 20 to 26 minutes behind the first malicious publish. Tanner Linsley and Manuel then ran containment at speed: the two @tanstack/history releases carried deprecation flags by 20:19, and the entire affected set followed by 21:03 — one hour and 43 minutes after publishing. By 21:30 the picture was complete: root cause pinned down, every cache flushed, a hardening pull request merged, and advisory GHSA-g7cv-rxg3-hmpx published as CVE-2026-45321 with a CVSS of 9.6 Critical. On the registry side, npm pulled the tarballs between 22:13 and 23:55, close to four and a half hours after the fact. The account drew 1,097 points and 465 comments on Hacker News. TanStack’s advice to users pulls no punches: any machine that installed an affected version on May 11 counts as compromised, and the AWS, GCP, Kubernetes, Vault, GitHub, npm, and SSH credentials on it all need rotating.
Five Fixes Maintainers Owe Their Pipelines
More instructive than the attack anatomy is the postmortem’s own accounting. Publishing ran with no internal monitoring or alerting attached, so discovery depended entirely on people outside. Every pull_request_target workflow deserves an audit plus repository_owner condition guards. Third-party actions should sit on commit SHAs rather than drifting refs like @main or @v6. Cache saves need explicit permissions standing in front of them. And the deepest change: OIDC trusted publishing itself is due for a rethink, because unreviewed automatic publishing buys too much convenience — move to short-lived classic tokens paired with manual review, or add provenance source verification. While they are at it, the seven-maintainer npm scope could stand to lose some blast radius.
