How 84 Malicious TanStack Packages Hit npm in 6 Minutes
On May 11, 2026, an attacker published 84 malicious versions across 42 @tanstack/* packages in under 6 minutes. Not a typo. Here is the exact chain that made it possible. 42 @tanstack packages compromised via GitHub Actions cache poisoning and OIDC token extraction

This is not a story about a zero-day. Every single technique used here was documented publicly before the attack. The attacker recombined existing research, chained three known vulnerabilities, and walked right through the door that was already open.
The Three-Part Chain
No single vulnerability made this possible. Each one bridges a trust boundary the next one needs.
1. pull_request_target Pwn Request
bundle-size.yml used the pull_request_target event trigger. This trigger runs in the context of the base repository, not the fork. It has access to secrets, cache, and write permissions that pull_request does not.
The workflow checked out the fork's PR code and ran a build against it:
on:
pull_request_target:
paths: ['packages/**', 'benchmarks/**']
jobs:
benchmark-pr:
steps:
- uses: actions/[email protected]
with:
ref: refs/pull/${{ github.event.pull_request.number }}/merge
- uses: TanStack/config/.github/setup@main
- run: pnpm nx run @benchmarks/bundle-size:build
That last run step executes arbitrary fork-controlled code with base-repo permissions. The author knew this was dangerous — there's a comment in the YAML noting the intent to keep benchmark-pr "untrusted with read-only permissions." The intent was right. The implementation missed something critical.
2. GitHub Actions Cache Poisoning
Setting permissions: contents: read does not prevent cache writes. actions/cache@v5 saves cache using a runner-internal token, not the workflow GITHUB_TOKEN. So the "read-only" workflow could still write to the shared pnpm cache.
The attacker's malicious vite_setup.mjs (smuggled into the PR as packages/history/vite_setup.mjs, ~30,000 lines of bundled JS) ran during the benchmark build and wrote a poisoned pnpm store to cache key:
Linux-pnpm-store-6f9233a50def742c09fde54f56553d6b449a535adf87d4083690539f49ae4da11
That key is computed from hashFiles('**/pnpm-lock.yaml'). The attacker crafted the cache entry to match exactly what release.yml would look up on the next push to main. Cache scope in GitHub Actions is per-repo, shared between pull_request_target runs and pushes. The PR running on fork code wrote to the same cache namespace that production workflows use.
Cache poisoning as a class was documented by Adnan Khan in May 2024. This is not a new attack surface.
3. OIDC Token Extraction from Runner Memory
release.yml declared id-token: write for npm's OIDC trusted-publisher binding. When the poisoned pnpm store was restored on the runner, attacker-controlled binaries were now on disk. They located the GitHub Actions Runner.Worker process, read /proc/<pid>/maps and /proc/<pid>/mem to dump worker memory, and extracted the OIDC token the runner had minted in memory.
That token was used to POST directly to registry.npmjs.org — bypassing the workflow's actual Publish Packages step entirely. The publish step never ran because tests failed. The malware did not need it.
The Python script used for memory extraction was verbatim from the tj-actions/changed-files compromise in March 2025, attribution comment and all.
What the Malware Actually Did
When a developer or CI environment ran npm install against any affected version, the package's prepare lifecycle script executed router_init.js (~2.3 MB, obfuscated, not in the package's files field).
That script:
- Harvested credentials from AWS IMDS / Secrets Manager, GCP metadata, Kubernetes service-account tokens, Vault tokens,
~/.npmrc, GitHub tokens, SSH private keys - Exfiltrated over the Session/Oxen messenger file-upload network (
filev2.getsession.org,seed{1,2,3}.getsession.org) — end-to-end encrypted, so no attacker-controlled C2 server to block by reputation - Self-propagated by enumerating packages the victim maintains on npm and republishing them with the same payload
If you ran npm install on an affected @tanstack/* package on May 11, 2026, treat the install host as compromised. Rotate AWS, GCP, Kubernetes, Vault, GitHub, npm, and SSH credentials reachable from it.
IOC Fingerprints
In any affected package.json:
"optionalDependencies": {
"@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c"
}
- File:
router_init.js(~2.3 MB, package root) - Cache key:
Linux-pnpm-store-6f9233a50def742c09fde54f56553d6b449a535adf87d4083690539f49ae4da11 - Exfiltration endpoints:
filev2.getsession.org,seed{1,2,3}.getsession.org - Payload commit:
79ac49eedf774dd4b0cfa308722bc463cfe5885c(orphan commit in fork network) - Attacker fork:
github.com/zblgg/configuration(renamed from TanStack/router to evade fork-list searches) - Forged commit identity:
claude <[email protected]>— not the real Anthropic Claude, a fabricated no-reply email
What TanStack Missed
Detection came from an external researcher at StepSecurity, 20 minutes after publish. Not from TanStack's own tooling. There was no internal alerting on npm publishes. For a project with 42 packages and 7 npm maintainers, that gap is worth thinking about.
A few other things that made this worse than it had to be:
npm unpublish is effectively unavailable at scale. Once packages have dependents, npm won't let you unpublish. The only option is deprecation and waiting for npm security to pull tarballs server-side. That added hours of exposure.
OIDC trusted-publisher has no per-step scope. Once id-token: write is on the workflow, any code running during that workflow can mint a publish-capable token. There is no "only the Publish Packages step can use this." The permission is workflow-wide.
Floating action refs are a standing risk. @v6.0.2 and @main on third-party actions mean an upstream compromise or a force-push can change what runs in your CI without a PR to your repo.
What to Actually Fix
TanStack published a hardening post the next day. The changes worth noting:
bundle-size.ymlrestructured so fork code never runs inpull_request_targetcontextrepository_ownerguards added to workflows- Third-party action refs pinned to SHAs
- All GitHub Actions caches purged across all TanStack repos
If you run a monorepo with similar CI patterns, audit every pull_request_target workflow you have. If any of them check out fork code and run a build, you have the same vulnerability.
The attack chain here used entirely public tradecraft, chained methodically, against a popular open-source project that many production apps depend on. Nobody invented anything new. The attacker just read the research and executed it.
That should bother you more than a zero-day would.
References
Comments
Leave a comment
Lovable Leaks Source Code: The $6.6B BOLA Vulnerability
An 8 million user platform ignored a critical BOLA vulnerability for 48 days. How a $6.6B AI app builder leaked source code, credentials, and user data.
Kubernetes vs Docker: Stop Comparing the Wrong Things
Docker builds containers. Kubernetes runs them at scale. They're not rivals and picking the wrong mental model for each costs you months of overhead.
Building a Personal MCP Server for Claude
Stop copy-pasting your bio. Build a custom Model Context Protocol (MCP) server in TypeScript to give Claude native access to your projects, resume, and personal context.
Tagged