On May 11, 2026, a worm published 84 malicious versions of 42 TanStack packages to npm, with valid provenance, from TanStack's own release pipeline. Two and a half hours later a Dependabot pull request pulled two of those versions into a small aviation-data project, and a single merge turned its maintainer's publish token into 110 more malicious versions in 95 minutes. The TanStack npm supply-chain attack is worth reading end to end because no password was phished and no step needed a human except one click.
TL;DR
-
Upstream: a fork's pull request ran code in a TanStack benchmark workflow and saved a poisoned cache. The release workflow restored it hours later, and the worm read a publish credential out of the runner's memory. Result: 84 versions across 42
@tanstack/*packages, all carrying valid SLSA provenance. -
Downstream: Dependabot opened a routine grouped bump 29 minutes after TanStack's public advisory. Two of its 13 updates were poisoned. The maintainer merged it 24 minutes later, the publish workflow ran
npm ciwith the token in scope, and the worm republished all 22 of his@squawk/*packages, five versions each. - Why it spread: npm runs dependency lifecycle scripts on install by default, deprecated versions stay installable, and npm refuses unpublish when a package has dependents. The bad TanStack versions stayed installable for up to four and a half hours after they were published.
- Blast radius: vendors counted over 160 packages ecosystem-wide, including Mistral's. The worm is known as Mini Shai-Hulud.
- The response was good. Both maintainers posted timestamped postmortems within a day, and the downstream pipeline was rebuilt within three.
What happened in the TanStack npm supply-chain attack
Everything below comes from two primary documents: TanStack's postmortem by Tanner Linsley (timestamps refined on May 15), and the downstream incident report by the maintainer of neilcochran/squawk, a set of aviation-data libraries (@squawk/airports, @squawk/notams, @squawk/weather, @squawk/mcp) with nine GitHub stars. The worm does not check stars.
The attack started in the morning, UTC. A renamed fork of TanStack/router opened PR #7378, "WIP: simplify history build", at 10:49. TanStack's bundle-size.yml workflow runs on pull_request_target, which executes in the context of the base repository. It checked out the fork's merge ref and built it, which ran the attacker's code. At 11:29 that code saved a 1.1 GB pnpm-store cache under the exact key the release workflow would later look up. At 11:31 the PR was force-pushed to an empty change, closed, and the branch deleted.
Eight hours later a legitimate merge triggered release.yml at 19:16. It restored the poisoned cache. Per the postmortem, attacker binaries read /proc/<pid>/mem of the Runner.Worker process, pulled out the OIDC token minted for id-token: write, and posted packages straight to the registry. The workflow's own publish step was skipped because tests failed. The malware published anyway, at 19:20 and 19:26: two versions per package.
TanStack did not find it. A StepSecurity researcher opened TanStack/router#7383, "Several npm latest releases were compromised", at 19:46, about 26 minutes after the first publish. At 21:19 @tan_stack posted the advisory:
How the Dependabot bump spread the worm
At 21:48, 29 minutes after that advisory, Dependabot opened squawk PR #246, "Bump the dev-dependencies group with 13 updates". Two of the thirteen were @tanstack/router-cli 1.166.40 → 1.166.49 and @tanstack/router-plugin 1.167.32 → 1.167.41. Both were compromised.
Dependabot did not auto-merge. It proposed; a person reviewed and merged at 22:12, 24 minutes after the PR opened. The incident report puts the rest in one sentence: "the publish workflow ran npm ci with NPM_TOKEN in scope. The malicious prepare script exfiltrated the token and used it to publish 5 malicious versions of every package the token had access to."
The token was "a single overly broad classic npm token". It reached all 22 @squawk/* packages and three unrelated personal ones. The first malicious version, @squawk/mcp@0.9.1, went out at 22:17. The last, @squawk/mcp@0.9.5, at 23:52. That is 110 versions in 95 minutes, and latest on every package pointed at the worm. The maintainer found out from npm's notification emails at 00:04.
Timeline of the attack (UTC)
| Time | Event |
|---|---|
| May 11, 10:49 | Fork opens PR #7378; pull_request_target workflow runs its code |
| 11:29 | Poisoned 1.1 GB cache saved under the release workflow's key |
| 19:16 |
release.yml restores the cache; the OIDC token is read from runner memory |
| 19:20 / 19:26 | 84 versions of 42 @tanstack/* packages published, valid provenance |
| 19:46 | StepSecurity opens TanStack/router#7383 |
| 20:19 → 21:03 | TanStack deprecates 2, then 28, then all 84 versions |
| 21:19 | @tan_stack advisory post |
| 21:48 | Dependabot opens squawk PR #246 (13 updates, 2 poisoned) |
| 22:12 | Maintainer merges; npm ci runs with NPM_TOKEN in scope |
| 22:13 | npm starts removing TanStack tarballs |
| 22:17 → 23:52 | 110 malicious @squawk/* versions published |
| 23:55 | npm's last TanStack removal (@tanstack/router-core) |
| May 12, 00:04 | Downstream maintainer revokes the token and disables Actions |
| 03:37–03:41 | GitHub Trust & Safety removes the 110 versions and resets latest
|
Note 21:03 and 22:12: every bad TanStack version was already deprecated when the downstream merge happened. Deprecated is only a warning; npm still installs it.
How does the Mini Shai-Hulud worm work?
StepSecurity deobfuscated the payload, a 2.3 MB obfuscated JavaScript file. I'll describe it only at the level of the published analyses.
It runs on install. The compromised TanStack versions add a hidden optionalDependency that points at an orphan git commit. npm fetches a git dependency as a tarball and runs its prepare script during install. A grouped-bump diff hides it: it says "router-cli 1.166.40 → 1.166.49" and nothing else.
It wants one thing: a token that publishes without a second factor. Its first step searches for a classic npm token with bypass_2fa: true. In CI it exchanges the GitHub OIDC token for a per-package publish token.
It asks the registry what else you own. With a publish credential in hand it queries npm's search for every package the maintainer controls, then publishes an infected tarball for each. Publishing is one HTTP request per version with no human step and no cooldown. That is why 95 minutes was enough for 110 versions.
It dresses as Dependabot. Its dead-drop commits use a fabricated author named "claude" (not Anthropic), the message chore: update dependencies, and branch names like dependabot/github_actions/format/fremen, then sandworm, harkonnen, atreides. StepSecurity: it "mimics Dependabot's branch naming convention". A bot proposed the version, a pipeline installed it, and the worm returned the favour by wearing the bot's uniform.
Why valid provenance did not help
The upstream packages carried valid SLSA Build Level 3 provenance. StepSecurity calls Mini Shai-Hulud "the first documented npm worm that produces validly-attested malicious packages". The attestation was correct: these tarballs really were built by TanStack's official pipeline. It just was not TanStack's code.
TanStack's follow-up post says it directly: "npm provenance, SLSA, OIDC, and 2FA all worked as advertised and still didn't stop this attack", and "no maintainer was phished, had a password leak, or a token stolen from their account." Provenance answers "which pipeline built this?". It cannot answer "was the pipeline clean?". Once attacker code runs inside the job that holds the credential, every signature it produces is genuine.
The same goes for OIDC: a short-lived token dies in minutes, but the worm read it out of memory while it was alive. The fix is keeping the install step and the credential in different jobs.
Who is to blame for the npm worm?
In the video I split it the way I split every postmortem, as a git blame of the production system. This is my own read of the sources, with no official standing.
- npm's install model, 55 %. Lifecycle scripts run on install by default. Deprecated versions stay installable. Unpublish is refused when dependents exist: TanStack's postmortem says this "adds hours of delay during which malicious tarballs remain installable". The first removal came 2 h 53 min after the first publish, the last 4 h 35 min after. And classic tokens that bypass 2FA exist, which is the worm's first search.
-
TanStack's CI, 25 %. A
pull_request_targetworkflow ran fork code with write access to a cache the release job trusted. Their own words: it "had not been audited despite being a long-known dangerous pattern", and "No internal alerting. We learned about the compromise from a third party." - The bump habit, 15 %. Thirteen updates proposed by a bot and merged in 24 minutes, into a publish workflow that ran install scripts with the publish token in the environment. The downstream report admits all three. The blame sits with the role, whoever fills it.
- Dependabot, 5 %. It did what it is built to do, 29 minutes after the advisory. The 5 % is for being so trusted that the worm copies its branch names.
The worm then kept going. Aikido counted "over 160 packages, including Mistral"; SafeDep reported TanStack, Mistral AI and 170 packages, and later a jump to PyPI.
How to protect your npm publish pipeline
The downstream maintainer's hardening post from May 14 is the most useful part of this incident, because it is four concrete PRs anyone can copy:
-
--ignore-scriptson everynpm ci(#253). -
publish.ymlsplit into a build job and a publish job, so the credential is never in the install environment (#254). - OIDC Trusted Publishing instead of a long-lived token: a credential that lives about 15 minutes (#256).
- A production-publish environment with a required reviewer (#259).
TanStack's side, from the follow-up: pnpm cache disabled in the release pipeline, all Actions caches removed on the affected workflows, third-party actions pinned to commit SHAs, repository_owner guards on workflows, and non-SMS 2FA enforced across npm and GitHub.
Here is the shape of the split, as a simplified sketch rather than either project's real file:
# publish.yml (illustrative sketch)
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read # no publish credential in this job
steps:
- uses: actions/checkout@<pinned-sha>
- run: npm ci --ignore-scripts
- run: npm run build
- uses: actions/upload-artifact@<pinned-sha>
with: { name: dist, path: dist }
publish:
needs: build
environment: production-publish # required reviewer
permissions:
id-token: write # short-lived OIDC, no NPM_TOKEN secret
steps:
- uses: actions/download-artifact@<pinned-sha>
with: { name: dist }
# publish the built artifact; no npm install runs in this job
Code from node_modules runs in build, which holds nothing worth stealing. The job that can publish never installs anything.
Two more habits from the Hacker News thread (1,097 points, 465 comments): "Trusted Publishing is not enough by itself", and a minimum release age, meaning a delay before a freshly published version is allowed into your tree. These bad versions were live for a matter of hours; a longer cooldown skips them.
Verdict: SHIP IT
I stamped the response SHIP IT. TanStack published a timestamped postmortem the same evening and corrected it on May 15. The downstream maintainer published his the next day, reverted the Dependabot PR in #248, paused @tanstack/* in dependabot.yml, and within three days his pipeline installed without scripts, split build from publish, and held no long-lived token.
The stamp is for the people. npm's defaults have not moved: scripts still run on install, and deprecated still means installable. So the Monday line from the episode: --ignore-scripts on install, and the publish token out of the job that runs it.
FAQ
What is Mini Shai-Hulud?
The name researchers gave the self-spreading npm worm behind the May 2026 TanStack compromise. It steals publish credentials during install and republishes every package the victim maintains with itself inside.
Was Dependabot hacked?
No. Dependabot proposed a normal version bump that happened to include two compromised TanStack versions. A human merged it. The worm only copies Dependabot's branch names to blend in.
Did npm provenance stop the TanStack attack?
No. The malicious versions carried valid SLSA provenance because they really were built by TanStack's release pipeline, after the pipeline itself was poisoned through a cache.
How do I know if I installed a compromised TanStack version?
TanStack's postmortem lists every affected package and version, all published on May 11, 2026 around 19:20 and 19:26 UTC. Check your lockfile against that list and rotate any credential that was present where the install ran.
Sources
- TanStack, "Postmortem: TanStack npm supply-chain compromise": https://tanstack.com/blog/npm-supply-chain-compromise-postmortem
- TanStack, hardening follow-up: https://tanstack.com/blog/incident-followup
- TanStack/router#7383, the detection issue: https://github.com/TanStack/router/issues/7383
- @tan_stack security advisory on X: https://x.com/tan_stack/status/2053948103766716630
- neilcochran/squawk PR #246, the Dependabot bump: https://github.com/neilcochran/squawk/pull/246
- neilcochran/squawk incident report (discussion #251): https://github.com/neilcochran/squawk/discussions/251
- neilcochran/squawk revert PR #248: https://github.com/neilcochran/squawk/pull/248
- neilcochran/squawk hardening post (discussion #264): https://github.com/neilcochran/squawk/discussions/264
- StepSecurity analysis: https://www.stepsecurity.io/blog/mini-shai-hulud-is-back-a-self-spreading-supply-chain-attack-hits-the-npm-ecosystem
- Aikido: https://www.aikido.dev/blog/mini-shai-hulud-is-back-tanstack-compromised
- SafeDep: https://safedep.io/mass-npm-supply-chain-attack-tanstack-mistral/
- Hacker News discussion: https://news.ycombinator.com/item?id=48100706
This article expands on an episode of **The Daily Diff, a five-minute daily video on what shipped and what broke in tech.
Watch the episode · Subscribe on YouTube · the written diff lands in your inbox every morning at thedailydiff.dev.



Top comments (0)