In six minutes on May 11, 2026, a threat group identified as TeamPCP published 84 malicious versions of 42 @tanstack/* packages to npm — not by stealing a developer's credentials, but by chaining three individually documented GitHub Actions weaknesses into a single, nearly invisible attack path that produced packages with valid SLSA provenance.
The Three-Stage Attack Chain That Bypassed Trusted Publishing
The compromise, detailed in TanStack's official postmortem and the GitHub security advisory GHSA-g7cv-rxg3-hmpx, required no persistent foothold and left almost no artifacts in node_modules. Each stage enabled the next.
Stage 1 — Entry via pull_request_target misconfiguration. The bundle-size.yml workflow used a pull_request_target trigger, which allows workflows initiated by fork pull requests to run with the permissions of the base repository rather than the fork. An attacker-controlled fork PR was sufficient to execute arbitrary code with elevated access. This trigger class is widely documented as dangerous when combined with checkout of untrusted code, yet it remains common in repositories that measure bundle size on contributor PRs.
Stage 2 — Cache poisoning. With workflow execution rights in the base repository context, the attacker injected a malicious vite_setup.mjs into the Linux-pnpm-store-* GitHub Actions cache key. Cache poisoning is effective here for a structural reason: cached artifacts are restored early in subsequent workflow runs, before most security controls or secret guards are applied. The poisoned entry would persist silently until a legitimate maintainer triggered a release.
Stage 3 — OIDC JWT extraction and impersonation. When a TanStack maintainer triggered a legitimate release from the main branch, the poisoned cache was restored as part of normal workflow setup. The malicious vite_setup.mjs then performed a runtime memory dump of the Actions runner process to extract the ephemeral OIDC JSON Web Token. That token — issued by GitHub's OIDC provider for trusted publishing to npm — was used to publish the malicious packages under the @tanstack scope with valid provenance attestations. The npm registry had no basis to reject the publication.
This is a meaningful design tension in the trusted-publisher model: eliminating long-lived npm tokens reduces one class of credential theft, but it concentrates trust in the ephemeral OIDC JWT. If that token is readable from runner memory during the same job that restores a poisoned cache, the provenance guarantee holds cryptographically while the underlying build environment has already been compromised. The StepSecurity analysis attributes this attack chain to TeamPCP; that attribution has not been independently confirmed by a government or law-enforcement body as of the time of this writing.
The chart below maps all three stages, their dependency on each other, and the artifacts each stage produced or consumed.
Six Minutes, 84 Packages: The Incident Timeline
The publication window was narrow. According to the TanStack postmortem and community reports, the first batch of 42 malicious packages appeared at 19:20 UTC on May 11, 2026. A second batch of the same 42 packages — this time tagged as latest on npm — was published at 19:26 UTC. Setting the latest tag is significant: it means any project running npm install @tanstack/<package> without a pinned version would have silently pulled a malicious build.
At 19:30 UTC, a community member filed GitHub Issue #7383, triggering an escalating response. By 20:15 UTC, TanStack maintainers had acknowledged the compromise and begun deprecating affected versions. The npm security team was engaged by 21:00 UTC to pull the affected tarballs.
The payload itself was designed to avoid detection during routine inspection. The primary file, router_init.js, weighed approximately 2.3 MB of obfuscated JavaScript — large enough to obscure its structure but not anomalous for a bundled JavaScript artifact. The file was placed at the package root and deliberately omitted from the files array in package.json, rendering it invisible to most automated inspection tools that audit declared package contents. A secondary helper, tanstack_runner.js, assisted execution. After completing its work, the payload exited with code 1 to suppress errors and avoid leaving artifacts in node_modules that a post-install audit might flag.
One fingerprint that survived this cleanup: a fake optionalDependencies entry in package.json pointing to github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c — an orphan commit on an attacker-controlled fork. This reference is now a reliable indicator of compromise in any package inspection.
The timeline below shows the six-minute publication window and the subsequent community-driven response.
Payload Behavior: Worm Logic, Encrypted Exfiltration, and a Dead-Man's Switch
Three payload behaviors distinguish this incident from a straightforward credential-stealing attack and raise the consequence level for any affected maintainer.
Worm propagation. Upon execution, router_init.js queried npm for all packages associated with maintainer:<victim> and attempted to re-publish each one with the same injection. A developer who maintains five packages becomes a vector for infecting all five. The GitHub security advisory confirms this behavior; the full scope of secondary infections, if any occurred, has not been disclosed at the time of publication.
Exfiltration via Session network. Stolen credentials — including AWS and GCP metadata service tokens, Kubernetes service account tokens, HashiCorp Vault tokens, ~/.npmrc contents, GitHub tokens, and SSH private keys — were exfiltrated using the Session messenger network (filev2.getsession.org, seed1.getsession.org, seed2.getsession.org). Session uses end-to-end encryption and does not expose a traditional command-and-control IP address. This means standard network-layer blocking of a C2 IP is not a viable defensive response; defenders must treat the exfiltration as complete for any environment where the payload executed, and rotate all secrets accordingly.
Dead-man's switch. The payload included a script, gh-token-monitor.sh, that continuously polled the stolen GitHub token's validity. If GitHub returned an HTTP 40x response — indicating the token had been revoked — the script triggered rm -rf ~/. on the victim's machine. This behavior means that revoking a stolen token without first preserving the affected environment for forensic review could trigger data destruction on the developer's local system. Defenders should plan their token rotation sequence carefully: isolate the machine, preserve logs, then rotate.
The data exfiltrated is of the class that enables persistent downstream access: cloud metadata tokens can be used to assume IAM roles; Vault tokens may expose secrets far beyond the npm ecosystem; SSH keys may provide access to internal infrastructure. Any organization whose developers or CI systems installed affected package versions should assume full secret compromise, not just npm credential compromise.
The reference card below lists every confirmed indicator of compromise from the TanStack postmortem and StepSecurity analysis for use in log review and endpoint inspection.
What Defenders Should Check Now
The remediation steps documented in the TanStack security advisory have three components that should be executed in a deliberate sequence.
Update first. Move to patched versions. The advisory identifies react-router@1.169.9 as an example patched release; check the advisory for the full list of affected and patched versions across all 42 @tanstack/* packages.
Preserve before revoking. Because the dead-man's switch destroys local data on token revocation, any developer or CI system that may have installed an affected version should isolate the potentially compromised machine and preserve a copy of GitHub Actions logs, Cloud audit logs, and shell history before touching credentials. Token revocation should follow isolation, not precede it.
Rotate broadly. Any secret accessible from an environment where an affected package executed should be treated as compromised: AWS and GCP credentials, Kubernetes service account tokens, HashiCorp Vault tokens, SSH keys, ~/.npmrc contents, and GitHub personal access tokens or fine-grained tokens. The scope of exfiltration via the Session network cannot be confirmed or bounded after the fact, so rotation should be treated as mandatory rather than precautionary.
For CI systems, audit GitHub Actions workflow runs from May 11, 2026 for any job that restored the Linux-pnpm-store-* cache and subsequently ran a release pipeline. Review Cloud audit logs for any API calls from Actions runner IPs around 19:20–21:00 UTC on that date.
For repository maintainers, the StepSecurity analysis recommends replacing pull_request_target triggers with pull_request in any workflow that does not explicitly require base-repository secrets, and pinning GitHub Actions cache restore steps to hashed action versions to prevent cache poisoning from surviving across workflow runs.
Comments (0)
Please sign in to join the discussion.
No comments yet.
Be the first to share your perspective on this topic.