Mini Shai-Hulud: The TanStack Supply Chain Attack That Defeated SLSA Provenance
How the Mini Shai-Hulud attack poisoned TanStack npm packages and bypassed SLSA Level 3 build provenance guarantees.
On May 11, 2026, between 19:20 and 19:26 UTC — a window of six minutes and 35 seconds — an attacker published 84 malicious versions across 42 @tanstack/* packages on npm. TanStack Router, TanStack Start, and a constellation of related packages downloaded millions of times per week were all compromised. No npm credentials were stolen. No maintainer accounts were phished. Every team member had 2FA enabled. The packages passed SLSA provenance checks and carried valid signed certificates. None of it mattered.
This is the story of Mini Shai-Hulud Wave 4 — the most sophisticated npm supply chain attack ever documented, attributed to TeamPCP, the same group behind the Trivy compromise in March and the Bitwarden CLI attack in April. If you installed any affected @tanstack package on May 11, 2026, your machine may be compromised right now.
Who Is TanStack and Why Does It Matter?
TanStack is one of the most widely used open-source JavaScript library suites in the ecosystem. TanStack Query alone has over 45 million weekly downloads. TanStack Router, the primary target of this attack, is the standard routing library for TanStack Start — a full-stack React framework with rapidly growing adoption. The packages affected in this attack collectively represent tens of millions of weekly installs across React, Vue, and SolidJS applications.
When a library at this scale is compromised, the blast radius is not measured in individual developers — it is measured in every CI/CD pipeline, every developer laptop, and every production server that ran npm install against any of the 42 affected packages during the six-minute window.
The Attack: Three Vulnerabilities Chained
ATTACK CHAIN OVERVIEW
Total publish window: 6 minutes 35 seconds | Detection: ~30 minutes after first publish | Detector: ashishkurmi / StepSecurity
The TanStack compromise was not a single exploit. It was a three-stage chain, each step building on the last, exploiting trust assumptions baked into the GitHub Actions ecosystem. TeamPCP named this toolchain Mini Shai-Hulud — after the sandworm from Frank Herbert's Dune — fitting for a worm that propagates through the npm ecosystem consuming everything in its path.
Stage 1 — The Pwn Request: pull_request_target Abuse
The attack began on May 10, 2026 when the attacker created a fork of TanStack/router under the account zblgg, deliberately renaming it to zblgg/configuration to evade fork-list searches. A malicious commit was then authored under the fabricated identity [email protected] — a deliberate choice designed to implicate AI tooling and create noise during forensic investigation.
The key exploit was a GitHub Actions workflow called bundle-size.yml. This workflow used the pull_request_target trigger — which runs in the context of the base repository rather than the fork, giving it access to repository secrets and cache. The workflow was intended to benchmark bundle sizes for incoming PRs from forks.
The workflow author tried to enforce a trust boundary, noting in comments the intent to keep the benchmark-pr job read-only. This attempt failed for two non-obvious reasons:
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 # fork code!
- uses: TanStack/config/.github/setup@main # calls actions/cache@v5
- run: pnpm nx run @benchmarks/bundle-size:build # executes fork-controlled codeThe checkout step pulls the fork's merged code, meaning attacker-controlled JavaScript runs inside the base repository's trusted CI context. Setting permissions: contents: read was not sufficient protection — and here is why.
Stage 2 — Cache Poisoning Across the Trust Boundary
GitHub Actions cache is scoped per repository and shared across all workflow triggers in that repository — including pull_request_target runs from fork PRs and push-to-main production runs. A critical misunderstanding caused the trust split to fail: actions/cache@v5's post-job save uses a runner-internal token, not the workflow's GITHUB_TOKEN. The permissions: field only controls GITHUB_TOKEN scope. Cache writes are not blocked by it.
The attacker's malicious vite_setup.mjs — a 30,000-line bundled JavaScript payload — ran during the benchmark workflow and wrote a 1.1 GB poisoned cache entry, keyed to exactly match what the release.yml workflow would look up on the next push to main. Cache key: Linux-pnpm-store-6f9233a50def742c09fde54f56553d6b449a535adf87d4083690539f49ae4da11.
After the cache was written, the attacker force-pushed the PR back to the current main HEAD — making the visible PR a 0-file no-op — then closed it and deleted the branch. The cache entry remained. Waiting.
Stage 3 — OIDC Token Extraction from Runner Memory
On May 11 at 19:15 UTC, a legitimate maintainer merged an unrelated CSS fix. The release.yml workflow triggered. It restored the poisoned 1.1 GB cache. The malware ran inside the trusted release context.
The payload located the GitHub Actions Runner.Worker process using /proc/<pid>/maps and /proc/<pid>/mem, extracted the OIDC token from runner process memory, and used it to authenticate directly to npm as the TanStack trusted publisher binding: TanStack/router release.yml@refs/heads/main.
The malicious packages were published with valid SLSA Build Level 3 provenance attestations — the first documented npm supply chain attack to produce validly-attested malicious packages. Every provenance verification tool gave these packages a clean bill of health. The workflow itself showed as failed. The malware had already run.
The Malware: What Mini Shai-Hulud Does to Your Machine
MALWARE EXECUTION FLOW — triggered by npm install lifecycle hook
1. HARVEST
~/.aws/credentials
~/.config/gcloud/
~/.kube/config
~/.vault-token
GITHUB_TOKEN
~/.npmrc tokens
~/.ssh/* keys
Docker config
Terraform creds
Claude Code creds
OpenAI/Anthropic keys
Crypto wallets
100+ file targets
2. PERSIST
systemd user service
cron entry (Linux)
LaunchAgent plist (Mac)
Claude Code hooks
VS Code extension
~/.bashrc injection
~/.zshrc injection
~/.profile injection
Polls GitHub token
every 60 seconds
Token revoked?
→ rm -rf ~/
3. EXFILTRATE
Primary:
Session Protocol CDN
filev2.getsession.org
E2E encrypted, no C2
IP blocking useless
Secondary:
GitHub GraphQL API
Dead-drop commits
[email protected]
Dependabot branch names
drawn from Dune novels
4. PROPAGATE
Query npm registry
for all packages
maintainer owns
Re-publish each with
malicious payload
using stolen creds
Cascaded to:
UiPath (80+ pkgs)
Mistral AI
DraftLab, OpenSearch
160+ packages total
The payload is router_init.js — a 2.3 MB heavily obfuscated JavaScript file. Once executed during npm install lifecycle hooks (via a prepare hook in a malicious @tanstack/setup optional dependency), it runs the following sequence.
Credential Harvesting — 100+ Targets
Cloud:
AWS: ~/.aws/credentials, ~/.aws/config, IMDS, Secrets Manager
GCP: ~/.config/gcloud/, metadata endpoint
Azure: ~/.azure/
Kubernetes: ~/.kube/config, /var/run/secrets/kubernetes.io/serviceaccount/token
Vault: ~/.vault-token, VAULT_TOKEN env
Developer tools:
GitHub: GITHUB_TOKEN env, ~/.config/gh/hosts.yml, .git-credentials
npm: ~/.npmrc
SSH: ~/.ssh/ (all private keys)
Docker: ~/.docker/config.json
Terraform: ~/.terraform.d/credentials.tfrc.json
AI tooling:
Claude Code: ~/.claude/credentials.json
OpenAI: OPENAI_API_KEY env
Anthropic: ANTHROPIC_API_KEY env
Crypto wallets, IDE configs, shell history, browser credentialsPersistence — Survives Reboots
The malware installs persistence across multiple vectors: a systemd user service and cron entries on Linux, a LaunchAgent plist on macOS. It also injects hooks into Claude Code, VS Code, and shell RC files (.bashrc, .zshrc, .profile). A polling daemon runs every 60 seconds watching the status of the victim's GitHub token.
Exfiltration — Designed to Evade Blocking
Primary channel: Session Protocol CDN (filev2.getsession.org, seed1/2/3.getsession.org) — end-to-end encrypted, no attacker-controlled C2, IP blocking is largely ineffective. Secondary channel: GitHub's own GraphQL API, where dead-drop commits are authored as [email protected] with Dependabot-style branch names drawn from Frank Herbert's Dune universe.
Self-Propagation: This Is a Worm
After harvesting credentials, the malware calls the npm registry API to enumerate every package the victim maintains. It then republishes each one with the malicious payload injected, using the stolen npm credentials. Each infected developer infects their entire package portfolio, which infects every other developer who installs those packages, which infects their portfolios, and so on. This is how the infection spread to UiPath, Mistral AI, DraftLab, OpenSearch, and 160+ additional packages within hours.
The Destructive Payload: rm -rf ~/
The most dangerous element is not the credential theft — it is what happens when you try to remediate. The polling daemon monitors for the victim's GitHub token to be revoked. When it detects revocation, it executes rm -rf ~/. Your entire home directory is wiped. TeamPCP deliberately weaponized standard incident response instincts. The remediation sequence below must be followed exactly.
Compromised Package List (Key Packages)
42 packages across 84 versions. Most widely used affected packages: @tanstack/react-router (1.169.5, 1.169.8), @tanstack/router-core (1.169.5, 1.169.8), @tanstack/solid-router (1.169.5, 1.169.8), @tanstack/vue-router (1.169.5, 1.169.8), @tanstack/react-start (1.167.68, 1.167.71), @tanstack/router-plugin (1.167.38, 1.167.41), @tanstack/start-server-core (1.167.33, 1.167.36). Secondary spread: @mistralai/mistralai (2.2.3, 2.2.4), @opensearch-project/opensearch (3.6.2), dozens of @uipath/* packages.
Confirmed clean (not affected): @tanstack/query*, @tanstack/table*, @tanstack/form*, @tanstack/virtual*, @tanstack/store.
How to Tell if You Are Affected
# Check lockfile for affected versions
grep -E '@tanstack/(react-router|router-core|solid-router|vue-router|react-start|router-plugin|start-server-core)' \
package-lock.json pnpm-lock.yaml yarn.lock 2>/dev/null
# Check for running malware processes
ps aux | grep -E 'tanstack_runner|router_init'
crontab -l | grep tanstack
ls ~/.config/systemd/user/ | grep tanstack
ls ~/Library/LaunchAgents/ | grep tanstack # macOSRemediation — Critical Order of Operations
WARNING: Do NOT revoke your GitHub token first. The malware watches for this and will immediately execute rm -rf ~/. Kill the daemon before revoking anything.
Step 1 — Kill the Daemon
# Kill malware processes immediately
pkill -f tanstack_runner 2>/dev/null
pkill -f router_init 2>/dev/null
# Remove persistence — Linux
systemctl --user stop tanstack.service 2>/dev/null
systemctl --user disable tanstack.service 2>/dev/null
rm -f ~/.config/systemd/user/tanstack.service
crontab -l | grep -v tanstack | crontab -
# Remove persistence — macOS
launchctl unload ~/Library/LaunchAgents/com.tanstack.runner.plist 2>/dev/null
rm -f ~/Library/LaunchAgents/com.tanstack.runner.plist
# Remove shell hooks
sed -i '/tanstack/d' ~/.bashrc ~/.zshrc ~/.profile 2>/dev/nullStep 2 — Rotate All Credentials (from a clean machine)
Rotate: GitHub personal access tokens and OAuth apps, npm access tokens, AWS credentials (IAM), GCP service account keys, Kubernetes service account tokens, Vault tokens, SSH private keys (generate new pairs, update authorized_keys everywhere), Docker Hub tokens, any API keys in environment variables on the affected host, contents of ~/.npmrc, ~/.aws/credentials, ~/.kube/config.
Step 3 — Check if You Spread the Worm
# If you maintain npm packages, audit your publish history for May 11
npm search --json maintainer:<your-npm-username> | jq '.[].name' | \
xargs -I{} npm view {} time --json | jq 'to_entries[] | select(.key | startswith("1."))'
# Any versions published on 2026-05-11 that you did not explicitly publish
# should be treated as compromised and deprecated immediatelyMitigations: Protecting Your Pipelines
1. Never run fork code with pull_request_target
# UNSAFE: runs fork code in base repo context with cache access
on:
pull_request_target:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }} # fork code
# SAFE: separate workflow, no base-repo privileges
on:
pull_request:
steps:
- uses: actions/checkout@v4 # checks out fork safely, no cache poisoning2. Pin all Actions to commit SHAs
# Vulnerable to tag-moving attacks
- uses: actions/checkout@v4
- uses: actions/cache@v5
# Immutable
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e26843. Use minimumReleaseAge in your package manager
Organizations with minimumReleaseAge configured were automatically protected — the compromised packages were deprecated before the aging delay expired. This single control would have blocked the attack for most users.
# Add to .npmrc or .pnpmrc
minimumReleaseAge=24h4. Restrict id-token: write to the specific publish job
jobs:
publish:
permissions:
id-token: write # only this job can mint OIDC tokens
contents: read
test:
permissions:
contents: read # no OIDC access5. Add Harden-Runner to your workflows
steps:
- uses: step-security/harden-runner@v2
with:
egress-policy: audit # detects unexpected outbound traffic
# Use 'block' to prevent exfiltration entirely6. Monitor your npm publish history
Set up alerting for any new version published under your npm account. Any publish you did not explicitly trigger should be investigated immediately. npm supports email notifications and webhook events for publish actions.
Why SLSA Provenance Failed — and What This Means
SLSA Build Level 3 is the current gold standard for supply chain security. TanStack had implemented it fully. The Mini Shai-Hulud attack defeated it entirely. The malware ran inside the legitimate release.yml workflow context, using a stolen OIDC token issued to TanStack/router release.yml@refs/heads/main — so every provenance attestation it generated was cryptographically valid.
This exposes a fundamental assumption in the SLSA threat model: that the CI environment running the build is trustworthy. When that environment is compromised through cache poisoning, the attestation certifies the wrong thing. Provenance shows where and how a build ran — it cannot verify the environment was clean. Wave 4 is the first attack to demonstrate this gap in practice.
This does not mean provenance is useless — it means provenance needs to be paired with runtime CI environment isolation, egress monitoring, and tamper-detection on the build environment itself.
The Pattern: TeamPCP's Escalating Campaign
TeamPCP, also tracked as DeadCatx3, PCPcat, ShellForce, and CipherForce, has been running Mini Shai-Hulud since September 2025. Each wave has increased in technical sophistication. Wave 4 is notable not for scale but for the provenance bypass — a capability no prior supply chain attack had demonstrated.
The thread connecting Trivy (March 2026), Bitwarden CLI (April 2026), and now TanStack (May 2026) is consistent: exploit trusted CI/CD pipelines rather than steal credentials directly, use the ecosystem's own trust mechanisms against itself. The campaign is ongoing. Additional packages continue to be discovered through worm propagation.
References: TanStack Official Postmortem | StepSecurity Mini Shai-Hulud analysis | OpenAI incident response | GitHub Security Advisory GHSA-g7cv-rxg3-hmpx | Snyk TanStack advisory | Orca Security analysis | Strobes technical breakdown | Appwrite deep-dive