Automated Threat Modeling

TanStack npm Supply Chain Compromise (CVE-2026-45321): Overview of a Chained CI/CD Attack That Published 84 Malicious Packages

In CVE-2026-45321, attackers chained GitHub Actions misconfigurations, cache poisoning, and OIDC token extraction to publish credential-stealing malware across 42 TanStack npm packages. This summary covers how the attack worked, how to detect it, and the indicators of compromise.

CVE Analysis

12 min read

ZeroPath CVE Analysis
ZeroPath CVE Analysis

2026-05-11

TanStack npm Supply Chain Compromise (CVE-2026-45321): Overview of a Chained CI/CD Attack That Published 84 Malicious Packages
Experimental AI-Generated Content

This CVE analysis is an experimental publication that is completely AI-generated. The content may contain errors or inaccuracies and is subject to change as more information becomes available. We are continuously refining our process.

If you have feedback, questions, or notice any errors, please reach out to us.

[email protected]

Introduction

On May 11, 2026, an attacker published 84 malicious package versions across 42 @tanstack/* npm packages in under six minutes. The versions carried credential-stealing malware, and they belong to one of the most widely used library families in the JavaScript ecosystem. The compromise is tracked as CVE-2026-45321, with a CVSS score of 9.6. None of its techniques is new. It chains three well-documented vulnerability classes into a single attack, precisely enough to bypass OIDC-based trusted publishing, SLSA provenance, and npm audit signatures at the same time.

TanStack provides routing, data fetching, table rendering, and form management libraries, mainly for React and Solid applications. The project's packages receive millions of weekly downloads, and its router library is a foundational dependency in a large number of production web applications. That reach made TanStack an attractive target for a supply chain attacker who wanted the widest possible downstream impact.

Technical Information

The attack chained three distinct vulnerability classes to get past standard security controls and publish malware under a trusted identity.

Phase 1: Pwn Request via pull_request_target

The attacker exploited a pull_request_target trigger in the bundle-size.yml workflow in the TanStack/router repository. The pull_request_target event is a well-documented footgun in GitHub Actions. It runs the workflow definition from the base branch, in the context of the base repository, with access to repository secrets and write permissions. If the workflow checks out and executes code from the pull request head, untrusted code runs with elevated privileges.

The attacker created a fork of TanStack/router on May 10, 2026 at 17:16 UTC and renamed it to zblgg/configuration to evade fork-list searches. On May 11 at 11:11 UTC, the attacker force-pushed a malicious commit onto the pull request head. When the bundle-size.yml workflow triggered, it checked out and ran the attacker's code in the privileged base repository context.

Phase 2: Cache Poisoning Across the Fork/Base Trust Boundary

While the untrusted code ran, the malware wrote a 1.1 GB payload into the pnpm store directory. The GitHub Actions cache mechanism then saved this poisoned store under a deterministic key:

Linux-pnpm-store-6f9233a50def742c09fde54f56553d6b449a535adf87d4083690539f49ae4da11

The key was crafted to match the cache key that the legitimate release workflow would later try to restore. The poisoned cache entry was saved at 11:29 UTC, and the payload sat there waiting for the next phase.

Phase 3: OIDC Token Extraction from Runner Memory

Later that day, a legitimate maintainer merged a separate pull request. The release workflow triggered and restored the poisoned cache, and the attacker-controlled binaries in the pnpm store executed during the build. The payload located the GitHub Actions Runner.Worker process and read its memory via /proc/{pid}/mem to extract the OIDC token.

The token was then used to authenticate direct POST requests to the npm registry, which bypassed the workflow's own publish step entirely. Because the legitimate runner in the legitimate repository had minted the token, the resulting packages carried valid SLSA Build Level 3 provenance attestations. Running npm audit signatures would not flag them.

Payload Architecture

The malware delivered its payload in two ways:

  1. An optionalDependencies entry was injected into each compromised package's package.json, pointing to a fictitious @tanstack/setup package hosted via a GitHub commit reference. This package's prepare script silently downloaded a Bun binary.

  2. A 2.3 MB obfuscated file called router_init.js was planted at the root of every compromised tarball. This primary payload read environment variables to detect whether it was running in a CI pipeline, then exfiltrated secrets to attacker-controlled infrastructure at filev2.getsession.org and seed1.getsession.org.

Attack Timeline

Time (UTC)Event
2026-05-10 17:16Attacker creates a fork of TanStack/router
2026-05-11 11:11Force push lands malicious commit on the pull request head
2026-05-11 11:29Poisoned cache entry saved to GitHub Actions
2026-05-11 19:20First wave of malicious packages published to npm
2026-05-11 19:26Second wave of malicious packages published to npm
2026-05-11 19:50External researcher reports the compromise

Each of the 42 compromised packages received exactly two malicious versions, published roughly six minutes apart.

Detection Methods

The compromise leaves a wide trail of indicators in package manifests, file systems, network traffic, CI/CD logs, and developer workstations. Each needs its own check.

Manifest and Lockfile Inspection

The single most reliable static indicator is an injected optionalDependencies entry in any @tanstack/* package's package.json that points to a fork commit:

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

This entry should never appear in legitimate TanStack packages. Any @tanstack/* dependency that references a github: URL with a pinned commit hash is a strong sign of compromise. To check your own projects:

# npm grep "@tanstack/" package-lock.json | grep -v node_modules # pnpm grep "@tanstack/" pnpm-lock.yaml # yarn grep "@tanstack/" yarn.lock # Also check for non-TanStack propagation targets grep -E "(draftlab|draftauth|taskflow-corp|tolka)" package-lock.json pnpm-lock.yaml yarn.lock 2>/dev/null

Cross-reference any resolved versions against the affected version lists published in GHSA-g7cv-rxg3-hmpx.

File System Artifacts

A ~2.3 MB file called router_init.js was planted at the root of every compromised tarball. The package's files field declares only ["dist", "src"], so a router_init.js at the package root is anomalous on its own:

find node_modules -name "router_init.js" -type f 2>/dev/null grep -r "@tanstack/setup" node_modules/*/package.json 2>/dev/null

The payload hashes (SHA-256) are confirmed across all compromised packages:

FileSHA-256
router_init.jsab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c
tanstack_runner.js2ec78d556d696e208927cc503d48e4b5eb56b31abc2870c2ed2e98d6be27fc96
@tanstack/setup package.json7c12d8614c624c70d6dd6fc2ee289332474abaa38f70ebe2cdef064923ca3a9b

Tarball size is another useful heuristic. Compromised tarballs are about 905 KB, against ~190 KB for clean versions, an increase of nearly 5x. Any SCA tool or registry proxy that tracks tarball sizes over time would flag the jump.

Network Indicators of Compromise

The malware uses two exfiltration channels, plus cloud metadata and second-stage download endpoints. Monitor or block the following:

C2 and exfiltration domains: filev2.getsession.org, seed1.getsession.org, seed2.getsession.org, seed3.getsession.org, api.masscan.cloud, git-tanstack.com

Second-stage payload URLs: https://litter.catbox.moe/h8nc9u.js, https://litter.catbox.moe/7rrc6l.mjs

Cloud metadata targets: 169.254.169.254 (AWS EC2 IMDS), 169.254.170.2 (ECS/Fargate task metadata), 127.0.0.1:8200 (local HashiCorp Vault)

Outbound HTTPS connections to filev2.getsession.org or api.masscan.cloud during npm install or build steps are a definitive sign that the payload executed. The Session Protocol CDN (getsession.org) is a legitimate privacy-focused service being abused here, so its domains may not appear on standard threat intelligence blocklists. Defenders should add them explicitly for this campaign.

The malware also pins against a specific TLS certificate: CN=seed1.getsession.org, O=Oxen Privacy Tech Foundation (Melbourne, AU, valid until 2033). TLS inspection tools can use this certificate fingerprint as an additional detection signal.

CI/CD Runtime Detection

In GitHub Actions environments, the payload runs a Python script that scrapes the Runner.Worker process memory via /proc/{pid}/mem to extract every secret, including masked ones. Detection signals include:

  • Unexpected python3 processes reading /proc/*/mem during CI runs
  • Process command lines showing Runner.Worker being targeted by another process
  • Execution of bun run tanstack_runner.js in CI process logs (the payload requires the Bun runtime, which is not standard in most CI environments)

The attacker's workflow runs that performed the malicious publishes are:

  • https://github.com/TanStack/router/actions/runs/25613093674 (attempt 4)
  • https://github.com/TanStack/router/actions/runs/25691781302

A secondary signal is the double-tap publishing pattern, in which each of the 42 compromised packages received exactly two malicious versions, published roughly six minutes apart. Publish-rate alerting that fires on two patch bumps for the same package in under 10 minutes would catch it.

Persistence Artifacts on Developer Machines

If the malware runs on a developer machine (via npm install), it drops several persistence mechanisms:

  • .claude/settings.json with a SessionStart hook that re-executes the malware on every Claude Code session
  • .vscode/tasks.json with a folderOpen task that re-executes the malware every time a VS Code workspace opens
  • .claude/router_runtime.js, .claude/setup.mjs, .vscode/setup.mjs as dropped payload and setup scripts
  • macOS: ~/Library/LaunchAgents/com.user.gh-token-monitor.plist for persistent GitHub token monitoring
  • Linux: ~/.config/systemd/user/gh-token-monitor.service performing the same function
  • .github/workflows/codeql_analysis.yml, an injected workflow disguised as CodeQL that exfiltrates all repository secrets on push or deployment events using ${{ toJSON(secrets) }}

Campaign-Specific Markers

The attacker leaves distinctive fingerprints attributed to the Dune-themed "Mini Shai-Hulud" campaign:

  • Git commits authored by [email protected] with the message chore: update dependencies (dead-drop commits for encrypted exfiltration data)
  • Branch names matching dependabot/github_actions/format/{dune-word} (e.g., atreides, fremen, sandworm, harkonnen, melange), designed to mimic legitimate Dependabot branches
  • npm tokens with the description IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner. Important: do not revoke such tokens before forensically imaging the affected machine. The payload reportedly includes a destructive wipe routine triggered by revocation.
  • Worm marker repositories: siridar-ghola-567, tleilaxu-ornithopter-43, described as "A Mini Shai-Hulud has Appeared"

Token Pattern Matching

The deobfuscated payload contains the regex patterns it uses to scan memory and files for credentials. Defenders can use the same patterns:

  • npm tokens: npm_[A-Za-z0-9]{36,}
  • GitHub tokens: gh[op]_[A-Za-z0-9]{36}
  • Vault tokens: hvs\.[A-Za-z0-9_-]{24,}
  • Kubernetes tokens: eyJhbGciOiJSUzI1NiIsImtpZCI6[\w\-.]+
  • AWS access keys: AKIA[0-9A-Z]{16}

SLSA Provenance Caveat

The compromised packages carry valid SLSA Build Level 3 provenance attestations. Running npm audit signatures will not flag them, because the attacker minted a legitimate OIDC token from the runner's own memory and used it with the Sigstore stack (Fulcio and Rekor) to generate valid attestations. Provenance confirms which pipeline produced the artifact. It does not confirm that the pipeline behaved as intended, so defenders should not treat SLSA provenance alone as a safety guarantee.

Attacker Infrastructure

Indicator TypeValue
GitHub accountszblgg (ID: 127806521), voicproducoes (ID: 269549300)
Attacker forkzblgg/configuration (renamed fork of TanStack/router)
Secondary forkvoicproducoes/router
Orphan payload commit79ac49eedf774dd4b0cfa308722bc463cfe5885c
Malicious commit in fork65bf499d16a5e8d25ba95d69ec9790a6dd4a1f14
Forged commit identityclaude <[email protected]>
Fictitious package@tanstack/setup
Payload filenamesrouter_init.js, tanstack_runner.js
Exfiltration domainsfilev2.getsession.org, seed1.getsession.org
Poisoned cache keyLinux-pnpm-store-6f9233a50def742c09fde54f56553d6b449a535adf87d4083690539f49ae4da11

Affected Systems and Versions

The compromise affected 42 distinct @tanstack/* packages on the npm registry. Each received exactly two malicious versions, for a total of 84 malicious versions published between 19:20 and 19:26 UTC on May 11, 2026. The complete list of affected packages and version numbers is in the security advisory GHSA-g7cv-rxg3-hmpx.

Treat any version of an @tanstack/* package installed or updated after 19:20 UTC on May 11, 2026, and before the affected versions were pulled, as potentially compromised. Versions published before 19:00 UTC on May 11, 2026 are confirmed clean.

The following package families have been confirmed clean and were not affected: @tanstack/query, @tanstack/table, @tanstack/form, @tanstack/virtual, @tanstack/store, and the @tanstack/start meta package.

Organizations whose CI/CD pipelines installed affected packages during the compromise window should treat those environments as breached. The malware was specifically designed to exfiltrate CI/CD secrets, cloud provider credentials, GitHub tokens, and SSH keys.

References

Detect & fix
what others miss

Works with
  • GitHub
  • GitLab
  • Bitbucket
  • Azure DevOps Services
  • Gerrit
  • CVS
  • Jira
  • Linear
  • Slack
  • Security Compass
Explore integrations
Security magnifying glass visualization