On May 11, 2026, between 19:20 and 19:26 UTC, the most sophisticated supply chain attack the npm ecosystem has ever seen took place. In just six minutes, 84 malicious versions were published across 42 packages in the @tanstack namespace. It wasn't a hacker stealing credentials. It was TanStack's own legitimate pipeline, using its verified identity, executing code that no one had written. And from there, the worm spread to Mistral AI, UiPath, OpenSearch, and over 160 additional packages.
The cruelest irony? The malicious packages carried valid SLSA provenance signatures. The tool designed to tell us "this package is safe" said exactly the opposite.
How it happened: three chained vulnerabilities
The attack — attributed to the TeamPCP group and dubbed "Mini Shai-Hulud" — didn't exploit a bug. It exploited three perfectly documented behaviors that, when combined, were lethal.
Step 1: The PR no one would suspect
On May 10, an attacker forked the TanStack/router repository under a new account. They opened a pull request with an innocent title: "WIP: simplify history build." The modified code looked trivial, but the repository's GitHub Actions workflow used the pull_request_target trigger.
Here's the problem: pull_request_target runs the workflow with the base repository's permissions, not the fork's. That is, the attacker's code — coming from an external fork — ran with access to the secrets, cache, and tokens of TanStack's official repository. The maintainers had tried to limit permissions, but underestimated one critical detail: actions/cache stores data using a runner's internal token, not the workflow's GITHUB_TOKEN. Read-only permissions don't protect the cache.
Step 2: Poisoning the GitHub Actions cache
The malicious code in the PR didn't exfiltrate data. It did something more subtle: it poisoned the pnpm cache under a predictable key — computed from the public pnpm-lock.yaml — that the release workflow would consume hours later. The poisoned cache weighed 1.1 GB and contained modified binaries. GitHub stored it without question.
That cache lay dormant for almost eight hours, until a legitimate push to main triggered the release workflow. The workflow restored the poisoned cache, and the attacker's binaries began executing inside TanStack's official runner.
Step 3: Stealing the OIDC token from process memory
The release workflow had the id-token: write permission, necessary for publishing to npm using OIDC trusted publishers. The malicious binaries used a technique documented since March 2025 in the tj-actions/changed-files attack: reading /proc/<pid>/mem of the runner process to extract the OIDC token from memory. With that token, they published directly to npm authenticated as TanStack.
The workflow tests failed. The legitimate publishing step never ran. But the malicious packages were already on npm, signed with SLSA provenance level 3. To any security tool, those packages were perfectly legitimate.
The worm that propagates itself
The payload of each malicious package — a 2.3 MB file called router_init.js — did four things upon installation:
Stole credentials: GitHub tokens, npm tokens, AWS credentials (via IMDSv2), GCP, Azure, Kubernetes service accounts, HashiCorp Vault tokens, and all system environment variables.
Self-propagated: identified other npm packages where the victim had publish access, injected the same malicious dependency, and published new compromised versions. Every infected developer or CI runner became a new infection vector.
Installed a persistent wiper: if it found a valid GitHub token with write access, it installed a daemon called
gh-token-monitorthat queried GitHub every 60 seconds. If the token was revoked, the daemon executedrm -rf ~/— deleting the user's entire home directory. On macOS it installed as a LaunchAgent; on Linux, as a systemd service. It self-deleted after 24 hours.Exfiltrated through three channels: stolen data was sent to the domain
git-tanstack.com, to nodes on the decentralized Session network (getsession.org), and to Dune-themed "dead drop" GitHub repositories.
The real damage
This isn't a theoretical attack. By the time npm started removing the malicious versions, the worm had already spread to over 169 packages with 373 malicious versions. The list includes:
- @tanstack/react-router (12.7 million weekly downloads)
- @mistralai/mistralai (Mistral AI's official SDK)
- @uipath (40+ packages from the UiPath ecosystem)
- opensearch-project/opensearch
- guardrails-ai on PyPI
- Dozens of packages for aviation data, authentication, MCP agents, and utilities
CVE-2026-45321 has a CVSS of 9.6 (Critical). And it's the fourth wave of a campaign that started in September 2025, each time more sophisticated. This time they achieved what no other had: malicious packages indistinguishable from legitimate ones by cryptographic signatures.
Five hard lessons for developers
1. pull_request_target is radioactive. If your CI workflow uses this trigger, assume that anyone on the internet can execute code with your repository's permissions. GitHub's documentation warns about it, but the warning is buried. If you need to test code from forks, use pull_request (without _target) or an ephemeral, isolated environment. Never, under any circumstances, execute code from a fork in a workflow that has access to secrets or the base repository's cache.
2. CI cache isn't innocent. GitHub Actions cache is shared between workflows. A malicious PR can poison it. A legitimate push can consume it. The solution: never cache binaries or dependencies that can be manipulated from a fork. Use unpredictable cache keys. And assume that anything an external PR can write, an attacker can control.
3. SLSA provenance isn't a malware detector. SLSA signatures prove the package was built by who it says it is. They don't prove the code inside is safe. If the legitimate pipeline is compromised, the signature is perfect and the package is poison. Provenance is a layer of trust, not a replacement for security analysis.
4. Your development tokens are the keys to the kingdom. The attack didn't exploit a vulnerability in your application. It exploited that you had an npm token with publish permissions in your CI, a GitHub token with repo access, and cloud credentials in environment variables. Use npm tokens with limited scope. Use OIDC instead of long-lived tokens. Rotate your credentials. And never run npm install on your development machine without thinking about what you're installing.
5. postinstall is a blind spot. Every time you install an npm package, its lifecycle scripts (preinstall, postinstall) run with your user's permissions. This attack used optionalDependencies to sneak in even on installations that didn't explicitly declare the dependency. If your project doesn't need dependencies to run scripts, disable them: npm config set ignore-scripts true. If you need them, at least audit what scripts run with npm install --dry-run.
For companies: this isn't a technical problem, it's a business problem
If your company uses JavaScript, TypeScript, Python, or any ecosystem with package managers, this attack affects you even if you don't use TanStack. The lessons aren't about a specific library — they're about how we trust the software supply chain.
Audit your dependencies now. Not next week. Check your lockfiles (package-lock.json, yarn.lock, pnpm-lock.yaml) for compromised package versions. Snyk, Socket, Orca, and other tools already have the signatures. If you find an affected version, assume that environment is compromised and rotate every secret it had access to.
Isolate your CI pipelines. Your CI/CD workflows shouldn't share cache between external PRs and internal pushes. Use separate environments for forks. Limit permissions to the bare minimum. If a workflow publishes to npm, let it be the only one with that privilege, and don't let it execute code from untrusted sources.
Prepare a response playbook. When — not if — the next attack happens, your team shouldn't be googling what to do at 11 PM. Define a clear process: who decides to revoke tokens, who audits environments, who communicates to customers. The wiper daemon in this attack is a brutal reminder that revoking tokens without first cleaning the system can be worse than doing nothing.
Invest in dependency hygiene. Fewer dependencies = smaller attack surface. Regularly audit what packages your team uses. Ask yourself if you need that 3-line micro-library that imports 40 others. Every dependency is a trust decision. Treat it as such.
The elephant in the room
The Shai-Hulud worm's source code was briefly published on GitHub before being removed. Mirror copies are already circulating. This means the attack won't stop here — other actors will iterate on it, as happened with Mirai.
The software supply chain is our industry's Achilles heel. We trust that thousands of packages we didn't write, maintained by people we don't know, built in pipelines we don't audit, will run on our servers and laptops without doing anything wrong. That trust is necessary to build software fast. But it shouldn't be blind.
The good news is that defenses exist. The bad news is that implementing them requires discipline, not special talent. And discipline, in security, is the scarcest resource.
Top comments (0)