The TanStack npm supply chain attack

On May 11, 2026, attackers published poisoned versions of 42 packages in the official @tanstack scope (84 malicious versions in total), part of a wider "Mini Shai-Hulud" campaign. Only the TanStack Router/Start packages were affected; the compromise was tracked as CVE-2026-45321 / GHSA-g7cv-rxg3-hmpx.

Scope: only the Router/Start monorepo was hit. TanStack Query, Table, Form, Virtual, and Store were not affected. Despite some early reports, those packages were confirmed clean.

What happened

Unusually, this was not a stolen npm token, phishing, or a typosquat. It was a chained attack on TanStack's own GitHub Actions CI/CD. An attacker abused a pull_request_target workflow that ran untrusted fork code, poisoned the project's Actions cache across the fork/base trust boundary, and, when a legitimate maintainer merge later triggered the release workflow, extracted the npm trusted-publisher (OIDC) token from the runner's memory and published the malicious versions directly to the registry. Because publishing used the project's legitimate publisher identity, the bad versions even carried valid provenance attestations.

On the consumer side, the poisoned packages pulled in their payload through an injected optionalDependency that pointed at a git URL, whose package.json declared a prepare lifecycle hook. That hook executed automatically on install (via the Bun runtime) and ran a credential harvester targeting cloud keys (AWS, GCP), Kubernetes and Vault tokens, npm and GitHub tokens, and SSH keys, with self-propagation and editor/agent persistence.

Detection and response

Automated scanners (StepSecurity, Socket) flagged the packages within minutes of publication: a tarball roughly 3.7× its normal size and an unexpected file at the package root were the tells. TanStack deprecated all 84 versions within about an hour and a half, and npm removed the tarballs registry-side within a few hours. If you installed any affected @tanstack/* Router/Start version on May 11, rotate credentials that were exposed to the install, pin to a pre-attack release, and reinstall from a clean lockfile.

The settings that reduce exposure

  • Dependency cooldown (minimum release age), the control that would have stopped this. The malicious versions were detected and pulled within minutes to hours. Any cooldown that holds back brand-new versions for a few days would have served only the known-good prior releases during the dangerous window, so a cooldown-protected install would never have resolved the poisoned versions.
  • Blocking exotic transitive deps (block-exotic-subdeps, pnpm). The payload was wired in through a transitive dependency pointing at a non-registry git source, exactly the class of dependency this pnpm setting restricts.
An honest caveat about install-script blocking. Because the payload ran via a prepare hook on a git-URL dependency (rather than a classic postinstall on the published package), a plain ignore-scripts setting would not have fully prevented this particular infection. It remains worthwhile defense-in-depth, and for the other packages in the same broader campaign that used a classic preinstall hook, it would have helped, but here the decisive control is the cooldown.

Harden your setup

DepsGuard checks whether a cooldown and the other built-in defenses are enabled for the package managers you have installed, and turns on the missing ones, with a diff preview and a backup before any change.

Sources