Software Supply Chain Security

TanStack's Build Pipeline Got Hijacked and Still Signed Valid SLSA Provenance (May 2026)

On May 11, 2026, attackers chained a pull_request_target abuse, cache poisoning, and OIDC token theft to publish 84 malicious @tanstack npm versions from TanStack's own trusted pipeline. It is the first npm compromise to carry valid SLSA provenance.

Safeguard Research Team
Threat Intelligence
11 min read

On May 11, 2026, between 19:20 and 19:26 UTC, 84 malicious package versions appeared across 42 packages in the @tanstack npm namespace. That part is unfortunately routine in 2026; npm namespace compromises happen most weeks. What makes the TanStack incident worth a careful read is how the packages were published. They were not pushed by an attacker who stole a maintainer's npm token. They were published by TanStack's own legitimate GitHub Actions release pipeline, using its real OIDC identity, after attacker-controlled code hijacked the runner mid-workflow.

The consequence is the detail that should reset some assumptions: this is the first documented case of a malicious npm package carrying valid SLSA provenance. Sigstore verified the build. The provenance attestation correctly stated which pipeline produced the artifact. Everything a downstream consumer would check to confirm "this came from the official TanStack build" passed, because it did come from the official TanStack build. The pipeline was telling the truth. It had just been turned against its owner.

This post walks through the verified attack chain as documented in TanStack's official postmortem and corroborated by StepSecurity and Snyk, explains precisely why SLSA provenance verified clean, and lays out what CI/CD owners and package consumers should change. The incident is attributed by StepSecurity to the threat group TeamPCP and is part of the self-propagating "Mini Shai-Hulud" campaign, but the propagation is not the lesson here. The pipeline-hijack-to-trusted-publish chain is.

TL;DR

  • On May 11, 2026 (19:20-19:26 UTC), 84 malicious versions across 42 @tanstack npm packages were published by TanStack's own release pipeline after attackers hijacked the runner.
  • The chain combined three flaws: a pull_request_target workflow that ran untrusted fork code without approval, GitHub Actions cache poisoning across the fork-to-base trust boundary, and in-memory extraction of an OIDC token from the runner process.
  • The malicious code used the stolen OIDC token to POST directly to registry.npmjs.org, bypassing the workflow's normal Publish step. Because publishing came from the legitimate runner with a valid OIDC identity, the artifacts received valid SLSA provenance.
  • Detection was fast: StepSecurity reported it at 19:46 UTC, roughly 20-26 minutes after publish. TanStack deprecated the full scope by 21:03 UTC; npm removed tarballs across May 12.
  • The payload harvested AWS, GCP, Kubernetes, Vault, and GitHub credentials, ~/.npmrc, and SSH keys, then exfiltrated via the Session encrypted messenger. It self-propagated; over 170 packages across npm and PyPI were eventually implicated.
  • The structural takeaway: SLSA provenance proves which pipeline built an artifact, not that the pipeline behaved as intended. OIDC trusted publishing has no per-publish review gate once configured.

What happened

TanStack's official postmortem lays out a tight timeline. The 84 malicious versions were published in a six-minute burst on May 11, 2026, from 19:20 to 19:26 UTC. External researcher ashishkurmi of StepSecurity reported the compromise at 19:46 UTC, roughly 20 to 26 minutes after the first malicious publish. TanStack issued its first deprecations at 20:19 UTC and had the full scope deprecated by 21:03 UTC. npm removed the malicious tarballs registry-side between 22:13 UTC on May 11 and the early hours of May 12.

The affected packages were core, widely-depended-upon TanStack libraries, including @tanstack/react-router, @tanstack/vue-router, @tanstack/react-start, and @tanstack/router-plugin, each in two malicious versions. Because the payload self-propagated (the "Mini Shai-Hulud" behavior), the blast radius did not stay inside the TanStack namespace. By the end of May 11, over 170 packages across npm and PyPI were confirmed implicated, with secondary victims including maintainers at Mistral AI and UiPath.

The headline technical finding, confirmed by both TanStack and StepSecurity, is that the malicious versions were published by TanStack's legitimate release pipeline using its trusted OIDC identity. As a result, the artifacts carried valid SLSA provenance. The build process was verified by Sigstore and the verification passed honestly.

How the attack worked

The attacker chained three distinct weaknesses to get from "opening a pull request from a fork" to "publishing from the trusted release runner with a valid OIDC token." Each step is individually known; the chain is what made it devastating.

Step 1: pull_request_target abuse (the "Pwn Request")

TanStack's bundle-size.yml workflow triggered on pull_request_target. Unlike pull_request, the pull_request_target trigger runs in the context of the base repository, with access to its secrets, but checks out and operates on code from the pull request, which can come from an untrusted fork. When a workflow built this way runs untrusted fork code without a manual approval gate, an attacker who opens a PR gets code execution in a privileged context. This is the well-documented "Pwn Request" pattern, and it is exactly what TanStack's bundle-size workflow exposed.

# Illustrative reconstruction of the dangerous pattern. Not the real workflow.
name: bundle-size
on:
  pull_request_target:          # runs with base-repo context + secrets
jobs:
  size:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@<sha>
        with:
          ref: ${{ github.event.pull_request.head.sha }}  # untrusted fork code
      - run: pnpm install && pnpm build  # executes attacker-controlled scripts

Step 2: cache poisoning across the trust boundary

GitHub Actions caches are scoped, but a fork's PR run can write a cache entry that a later run in the base repository will restore. The malicious commit injected a large obfuscated file (TanStack's postmortem describes a roughly 30,000-line vite_setup.mjs; Rescana described a 2.3 MB router_init.js) into the pnpm store during the PR CI run. That poisoned cache entry persisted. When the trusted release workflow later restored the pnpm cache, it pulled in the attacker's code. This is the pivot from "code execution in a PR sandbox" to "code execution in the release pipeline."

Step 3: OIDC token extraction and direct publish

Once running on the release runner, the attacker's code dumped the GitHub Actions worker process memory and extracted the OIDC token that the workflow used for npm trusted publishing. It then used that token to authenticate POST requests directly to registry.npmjs.org, publishing the 84 malicious versions while bypassing the workflow's own Publish Packages step entirely.

This is the crux. The publish came from the genuine runner, authenticated by the genuine OIDC identity, so npm's trusted-publisher flow and the resulting SLSA provenance attestation were all valid. As the reporting put it, Sigstore verified the build process correctly; what SLSA does not guarantee is that the code being built was safe. SLSA provenance confirms which pipeline produced an artifact, not whether the pipeline was behaving as intended.

The payload

The injected code harvested AWS credentials (via instance metadata and Secrets Manager), GCP metadata, Kubernetes and Vault tokens, the contents of ~/.npmrc, GitHub tokens from environment variables, the gh CLI, and git-credentials, and SSH private keys. Exfiltration went over the Session encrypted messenger network, meaning the C2 traffic was end-to-end encrypted and the operators never exposed plaintext infrastructure. Reporting also described a persistence daemon (gh-token-monitor) installed as a systemd user service or macOS LaunchAgent that polled GitHub and self-destructed on token revocation, which matters for cleanup ordering (below).

What detection looks like

The hard truth is that the controls most teams point at "is this package authentic" all passed. Detection has to move up a level, to the pipeline's behavior:

  • A publish to your npm namespace that did not originate from your workflow's designated Publish step. If you can correlate registry publish events with your CI run logs, a publish with no matching Publish-step log line is a strong signal of mid-workflow hijack.
  • Workflows triggered by pull_request_target that check out head.sha and run build or install scripts without an if guard restricting to trusted authors or a manual approval environment. This is the entry condition; audit for it proactively.
  • Unexpected large files appearing in dependency caches or lockfiles, especially obfuscated JavaScript injected via optionalDependencies or a build-step artifact that the source tree does not explain.
  • A persistence process named gh-token-monitor, or any unexpected systemd user service / LaunchAgent on CI hosts and developer machines that polls GitHub on a fixed interval.
  • Outbound traffic from CI runners to Session messenger seed/storage domains (for example filev2.getsession.org) or to other unexpected destinations during a build.

What to do Monday morning

Ordered by urgency.

  1. Audit every workflow for pull_request_target that executes untrusted fork code. Add if: github.repository_owner == '<your-org>' guards, require a manual-approval environment for fork PRs, or split size/preview checks into a non-privileged pull_request workflow that holds no secrets. This closes the entry door.
  2. If you publish via OIDC trusted publishing, add per-publish verification. Trusted publishing has no human review once configured; any code path in the workflow can mint a publish-capable token. Verify that publishes originate only from the expected workflow step (provenance-source verification), or move sensitive releases to short-lived classic tokens with a manual review gate, as TanStack's postmortem recommends.
  3. Purge and scope your Actions caches. Cross-boundary cache poisoning was the pivot. Purge existing cache entries after any suspected fork-PR abuse and ensure release workflows do not blindly restore caches that PR runs could have written.
  4. If you consumed affected @tanstack versions, remove the persistence daemon before rotating tokens. gh-token-monitor was reported to self-destruct (and in some descriptions wipe files) on detecting token revocation. Find and kill the daemon and its service definition first, then rotate. Rotating before removal can trigger destructive behavior.
  5. Rotate all credentials reachable from affected runners and developer machines. Cloud keys, Kubernetes and Vault tokens, npm tokens, GitHub tokens, and SSH keys. Treat anything the build could read as burned.
  6. Pin third-party actions to commit SHAs and add repository_owner guards. These are among the hardening steps TanStack itself adopted post-incident.
  7. Block known C2 domains at DNS/proxy. Even with encrypted transport, blocking the documented Session seed/storage and secondary domains limits exfiltration and propagation.

Why this keeps happening

The industry spent five years building cryptographic provenance so that consumers could verify an artifact came from a legitimate build. That work is genuinely valuable, but it answered a narrower question than many teams assumed. Provenance and signing answer "did this come from the pipeline it claims to come from." They do not answer "was that pipeline doing what its owner intended at the moment it built this." TanStack proved the gap is exploitable: hijack the pipeline mid-run and every downstream signature comes out valid.

The second recurring failure is the pull_request_target trap. It exists because maintainers want fork PRs to run useful checks that need repository secrets (bundle size comments, preview deploys). The trigger that enables that convenience also grants untrusted code a foothold in a privileged context. Combined with mutable, cross-boundary caches and OIDC tokens sitting in runner memory, a single over-permissive convenience workflow became a path to the crown jewels.

OIDC trusted publishing made the final step clean. It removed the long-lived npm token (good) but replaced it with an in-process token that any code on the runner can read and that mints publish rights with no per-publish human in the loop. Convenience again, traded against a review gate that turned out to matter.

The structural fix

The defense that would have shortened dwell time here is treating "where did this artifact actually get published from inside the pipeline" as a first-class signal rather than trusting the provenance attestation alone. Safeguard's SBOM and provenance tooling records the expected publish path for your releases and can flag when an artifact's provenance is technically valid but its publish did not flow through the designated, reviewed step, the exact mismatch that made the TanStack publishes look legitimate. Reachability analysis prioritizes which of your repositories expose privileged OIDC and cloud credentials to fork-triggered workflows, so you fix the pull_request_target entry conditions that matter most first. And continuous SCA monitoring against your dependency graph means that when a trusted upstream like @tanstack/react-router ships a compromised version, you learn your exposure in minutes rather than after a propagation wave. None of this would have stopped TanStack's own pipeline from being hijacked, but it would have caught the unexpected publish path and narrowed the consumer-side blast radius.

What we know we don't know

There is no CVE for this incident; it was an attack chain across configuration and trust boundaries, not a single product vulnerability. Reporting differs slightly on the injected file name and size (vite_setup.mjs per TanStack's postmortem versus router_init.js per Rescana), which likely reflects different stages of the same multi-file payload rather than a contradiction; treat the exact artifact names as reported-not-confirmed. The full count of downstream consumers who installed a malicious version before deprecation is not public, and because exfiltration used end-to-end-encrypted Session transport, the operators' downstream use of the stolen credentials is not observable from the public record.

References

Internal reading:

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.