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.

Dark server room representing the TanStack npm supply chain compromise by TeamPCP
📌
🛡️ Supply Chain Security • ColibriSec Knowledge Base

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

Phase 1 (May 10)  →  Fork created → Malicious PR opened (pull_request_target) → 1.1 GB cache poisoned → PR closed, traces removed
Phase 2 (May 11 19:15) → Legit PR merged, release.yml fires → Poisoned cache restored → OIDC token extracted from /proc/mem → 84 malicious packages published in 6m 35s
Phase 3 (19:20→)    → Credentials harvested (100+ paths) → npm packages enumerated → Worm republishes to 160+ packages → rm -rf ~/ if token revoked

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 code

The 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 credentials

Persistence — 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   # macOS

Remediation — 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/null

Step 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 immediately

Mitigations: 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 poisoning

2. 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@5a3ec84eff668545956fd18022155c47e93e2684

3. 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=24h

4. 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 access

5. 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 entirely

6. 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

Read more

Brecha de Datos Médicos en Photon Health

Filtración en Photon Health: Zero-Day de Inyección SQL en Metabase Expone Recetas Médicas de Pacientes

📌Security Roundup Series: Semana del 9 de Octubre de 2026 • 4 min read deep dive🏛️Incident Overview: Target / Organization: Photon Health, Inc. (Plataforma de Prescripción Médica Digital) Threat Actor / Attribution: Actor Desconocido (Extorsión Financiera) Impact / Records Compromised: Nombres de pacientes, direcciones, números de teléfono, fechas de nacimiento, recetas médicas completas

By James Luther