TanStack and the provenance gap

On May 11, 2026, malicious npm artifacts were signed by TanStack's legitimate OIDC pipeline. Sigstore verified them. Provenance-only checks would have accepted them. Release-age cooldowns cover a different failure mode.

TL;DR

On May 11, 2026, attackers published 84 malicious npm artifacts across 42 @tanstack packages. Within 48 hours, the same campaign reached 170+ package names across npm and PyPI. The malicious versions were signed by TanStack’s legitimate OIDC publishing pipeline. Sigstore verified them. Provenance attestation showed them as authentic. Checks that only verify signing identity would have accepted them.

A 4-day release-age cooldown would have blocked fresh installs of those versions while the takedown happened. That is the useful property: it does not decide whether a package is honest. It refuses to install brand-new versions until the ecosystem has had time to react.

This is part of the same pattern I have been tracking since the original Shai-Hulud worm: short malicious publish windows, fast credential harvesting, and defenses that look good on paper but do not help the machine that installs during the window.

What happened

On Monday May 11, 2026, between 19:20 and 19:26 UTC, 84 malicious npm package artifacts were published across 42 packages in the @tanstack namespace (Aikido, Snyk, Socket).

TanStack is one of the larger framework ecosystems in JavaScript. @tanstack/react-router alone has around 12 million weekly downloads. @tanstack/react-query, @tanstack/react-table, and the rest of the family sit in the dependency tree of an enormous number of React applications. A malicious publish in that namespace reaches a lot of machines quickly.

The malicious versions executed at install time, harvesting credentials from ~/.npmrc, ~/.aws/credentials, ~/.ssh/, GitHub Actions environment variables, HashiCorp Vault tokens, and any cloud credentials they could find in the runner’s process tree. The harvested credentials were then used to attempt onward compromise of additional packages, which is why the campaign reached 170+ package names across npm and PyPI within 48 hours rather than staying contained to TanStack (Wiz, Endor Labs).

Socket’s automated scanner flagged the artifacts within 6 minutes of publication. The TanStack team and npm pulled the versions later that evening. By then the campaign was already moving.

This is wave 4 of the Shai-Hulud campaign, the self-replicating worm that first appeared on npm in September 2025. The cadence so far:

  • September 15, 2025: Shai-Hulud (original), hundreds of packages.
  • November 2025: Shai-Hulud 2.0, 25,000+ malicious GitHub repos, Zapier / PostHog / Postman among the named hits.
  • March 2026: Trivy npm packages, attributed to TeamPCP.
  • April 22, 2026: @bitwarden/cli, malicious for 93 minutes, also TeamPCP.
  • May 11–13, 2026: TanStack / Mini Shai-Hulud wave 4, 170+ package names.

The attack chain

What makes this one different is not the payload. It is how the malicious versions were published.

TanStack does not let humans push directly to npm. Releases happen through a trusted-publishing pipeline: a GitHub Actions workflow with an OIDC identity that npm accepts as the legitimate publisher. The workflow signs each artifact with Sigstore and publishes the provenance attestation to npm. This is the recommended modern hardening posture. It is what every supply-chain hardening guide for the last two years has told maintainers to set up.

The attackers did not bypass it. They became it.

The chain, in order:

  1. pull_request_target Pwn Request. TanStack had a workflow that ran on pull_request_target and checked out the PR’s head ref before running. pull_request_target runs with the privileges of the base repository, not the fork. Combined with checking out untrusted code from a fork, this lets a malicious PR execute arbitrary code with the workflow’s secrets and tokens. This is a known anti-pattern, well documented by GitHub themselves, but it is also a pattern that is easy to introduce by accident in any workflow that wants to interact with PRs from forks.

  2. GitHub Actions cache poisoning. Once the malicious PR’s code was running in the privileged context, the attackers used it to poison the Actions cache for the repository. Cached artifacts persist across workflow runs and are not scoped to PRs. Future runs of the release workflow would pull the poisoned cache.

  3. OIDC token extraction from runner process memory. When the release workflow ran next, it requested an OIDC token from GitHub’s identity provider to authenticate as the trusted TanStack publisher to npm. The poisoned cache included tooling that read this token out of runner process memory at the exact moment it existed. The token has a short lifetime, but it does not need to last long. It needs to last one npm publish call.

  4. Publish through the legitimate pipeline. With the stolen OIDC token, the attackers called npm’s publish API as TanStack’s trusted publisher identity. npm accepted the request because the OIDC token was valid and freshly issued by GitHub. Sigstore signed the artifact because that is what the trusted-publishing flow does. The provenance attestation was attached because that is also what the flow does. The malicious version landed on npm with the expected publisher identity and provenance.

The attackers did not forge TanStack’s identity. They borrowed it for as long as the OIDC token was alive, and used it through the real TanStack publish path. Checks that only ask “was this signed by the expected publisher?” return yes in that situation.

What was supposed to stop this

Most of the normal controls did what they were supposed to do. They still did not stop a consumer install during the active window.

DefenseWhat it doesWhat happened on May 11
2FA on maintainer accountsStops account takeoverDid not apply. No human credentials were stolen.
Trusted publishing with OIDCReplaces long-lived tokens with short-lived onesThe short-lived token was stolen while it was alive. Its short lifetime did not help.
Sigstore provenance attestationProves an artifact was built by a specific workflowThe artifact was built by that specific workflow. Provenance was accurate.
npm provenance verification on installRequires packages to have provenanceThe malicious package had provenance.
npm auditSurfaces known vulnerabilitiesAt install time, the package was not yet in the advisory database.
GitHub Actions hardeningReduces supply-chain risk in CIThe Pwn Request + cache poisoning chain bypassed most of the standard hardening.

Detection worked quickly. Socket flagged the artifacts within 6 minutes. The TanStack team and npm pulled the versions later that day. That still leaves the machines that installed during the window. If your CI ran a fresh install between 19:20 UTC on May 11 and the takedown, the runner pulled the malicious version.

What a release-age cooldown does

A release-age cooldown tells your package manager to refuse versions newer than N days. With a 4-day window, a fresh install during the TanStack publish window would not take the malicious version. By the time the window lifts, the version is gone.

That does not make cooldowns a detector. They do not know whether a package is honest. They just keep your machine out of the short interval where new malicious versions are live and not yet pulled.

The trade-off is also real: security fixes are fresh versions too. You need an override path for urgent fixes. The point is not “never install new code”; it is “do not install new code by accident five minutes after it was published.”

pkg-quarantine is my wrapper for this. quarantine init writes the native settings where package managers support them. quarantine audit --exit-code checks that the settings are actually enforced, which matters because a configured-looking file can still be ignored.

The TanStack wave also points at a gap the tool does not close yet: after-the-fact inspection. If a machine already installed a malicious version, a release-age gate is too late. The next useful command is quarantine doctor: scan installed dependencies against known incident IoCs and tell me when I am already dirty.

Try it

npm install -g @happyberg/pkg-quarantine
quarantine init
quarantine audit

References