Back to News & Insights
JavaScript October 1, 2026 · 10 min read

TanStack npm supply-chain attack: how a Dependabot bump spread a worm

TanStack npm supply-chain attack, step by step: a poisoned CI cache, 84 bad versions, then a Dependabot bump that republished 22 packages in 95 minutes.

TanStack npm supply-chain attack: how a Dependabot bump spread a worm

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 ci with 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.

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 pullrequesttarget, 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//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 @tanstack posted the advisory:

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 NPMTOKEN 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.

| Time | Event | |---|---| | May 11, 10:49 | Fork opens PR #7378; pullrequesttarget 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 | @tanstack advisory post | | 21:48 | Dependabot opens squawk PR #246 (13 updates, 2 poisoned) | | 22:12 | Maintainer merges; npm ci runs with NPMTOKEN 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.

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 bypass2fa: 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/githubactions/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.

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.

Want to discuss this further?

Book a free strategy call with our team to see how these insights apply to your specific business goals.

Book a consultation