πŸ”’DevSecOps11 min read4 reads

TanStack npm Attack Hits CISA KEV: What to Do Now

CVE-2026-45321 entered CISA's KEV on May 27, 2026. Here is exactly how TeamPCP hijacked TanStack's CI pipeline to publish 84 malicious npm packages, plus concrete steps JS/TS shops need now.

A

Admin

May 30, 2026

Share:
TanStack npm Attack Hits CISA KEV: What to Do Now

TanStack's npm publish pipeline produced the malware itself. The packages carried valid SLSA Build Level 3 provenance attestations. CISA added CVE-2026-45321 to its Known Exploited Vulnerabilities catalog on May 27, 2026 β€” sixteen days after 84 malicious package versions landed in the npm registry under one of the most trusted namespaces in the JavaScript ecosystem.

If your React or full-stack project pulls from @tanstack/*, this is not a theoretical risk. More than 4,000 downloads of the poisoned versions occurred before npm removed the tarballs. Every CI runner that ran npm install against an affected version during the window of May 11, 2026 between 19:20 and 19:26 UTC must be treated as potentially exfiltrated.

What Actually Happened on May 11

The attackers β€” attributed by StepSecurity to a group called TeamPCP β€” did not steal an npm password or a long-lived publish token. TanStack had already eliminated long-lived tokens by migrating to GitHub's OIDC trusted-publisher integration. That was the right call. The attackers went around it.

Three Chained Weaknesses

The attack chained three well-documented GitHub Actions vulnerability classes into one continuous exploit path:

  1. pull_request_target misconfiguration (Pwn Request): A workflow triggered by pull_request_target runs in the context of the base repository, not the fork. This means it has access to repository secrets and OIDC identity β€” even when the triggering commit comes from an untrusted fork. The attacker submitted a pull request from an attacker-controlled fork containing modified workflow code.

  2. GitHub Actions cache poisoning across fork↔base trust boundary: The attacker's fork code wrote poisoned data into a cache key that the base repository's release workflow later restored without validation. The cache key Linux-pnpm-store-6f9233a50def742c09fde54f56553d6b449a535adf87d4083690539f49ae4da11 was the specific poisoned entry.

  3. OIDC token extraction from runner memory: Once the attacker's payload ran inside the base repository's workflow context, it extracted the short-lived OIDC token from the GitHub Actions runner process memory. That token β€” scoped to TanStack's npm trusted-publisher identity β€” was then used to publish 84 malicious package versions across 42 packages in the @tanstack namespace in approximately six minutes.

The npm account itself was never compromised. The Sigstore attestations on those malicious packages are genuine: they correctly attest that the packages were built and published by release.yml running on refs/heads/main in the TanStack/router repository. The build was authorized by the pipeline. The pipeline had been compromised. That is the architectural gap this attack exposes.

Detection Timeline

An external researcher at StepSecurity detected the malicious versions publicly within 20–26 minutes. All 84 versions were deprecated within the hour. npm removed the tarballs shortly after. The initial detection was through Sonatype's automated integrity scanning, which flagged the anomalous payload on May 18, 2026 β€” seven days after publication β€” which is the delay that allowed thousands of downloads before the full registry removal.

Affected Packages and Versions

The compromised set covered 42 packages in the router and start monorepo. Each received exactly two malicious versions, published minutes apart. The clean families β€” @tanstack/query*, @tanstack/table*, @tanstack/form*, @tanstack/virtual*, @tanstack/store β€” were not affected.

Package Malicious Versions Fixed Version CVE
@tanstack/react-router 1.169.5, 1.169.8 β‰₯ 1.169.9 CVE-2026-45321
@tanstack/router-core 1.169.5, 1.169.8 β‰₯ 1.169.9 CVE-2026-45321
@tanstack/router-plugin 1.169.5, 1.169.8 β‰₯ 1.169.9 CVE-2026-45321
@tanstack/start-server-core 1.169.5, 1.169.8 β‰₯ 1.169.9 CVE-2026-45321
@tanstack/start-client-core 1.169.5, 1.169.8 β‰₯ 1.169.9 CVE-2026-45321
@tanstack/react-router-devtools 1.169.5, 1.169.8 β‰₯ 1.169.9 CVE-2026-45321
@tanstack/react-start 1.169.5, 1.169.8 β‰₯ 1.169.9 CVE-2026-45321
@tanstack/history 1.169.5, 1.169.8 β‰₯ 1.169.9 CVE-2026-45321
(34 additional @tanstack packages) same pattern β‰₯ 1.169.9 CVE-2026-45321

The full affected list is in the GitHub Security Advisory GHSA-g7cv-rxg3-hmpx. CVSS score: 9.5 (Critical).

What the Malicious Payload Did

The poisoned packages contained an obfuscated ~2.3 MB file named router_init.js at the package root. When npm install ran, the malicious optionalDependencies entry fetched an orphan payload commit (79ac49eedf774dd4b0cfa308722bc463cfe5885c) from the fork network, executed a prepare lifecycle script, and ran the script.

Credentials harvested by router_init.js:

  • AWS IMDS and Secrets Manager credentials
  • GCP metadata endpoint tokens
  • Kubernetes service-account tokens
  • HashiCorp Vault tokens
  • ~/.npmrc publish tokens
  • GitHub personal access tokens and OIDC trust configurations
  • SSH private keys

Exfiltration went over the Session messenger file-upload network: filev2.getsession.org and seed{1,2,3}.getsession.org.

The forged commit identity used claude <claude@users.noreply.github.com> as the author name. The actual attacker GitHub accounts were zblgg and voicproducoes.

The Worm That Keeps Publishing

TeamPCP built Mini Shai-Hulud as a self-propagating supply chain worm, not a one-shot exfiltration. After stealing credentials from an install host, the payload:

  1. Enumerates npm packages the victim account has publish access to.
  2. Injects the same malicious optionalDependencies entry into those packages.
  3. Mints a new npm publish token using the stolen OIDC trust and publishes poisoned versions.
  4. Uses the GitHub GraphQL API to commit copies of the worm into branches of the victim's source repositories.

Within 48 hours the campaign expanded to more than 160 additional npm and PyPI packages, reaching Mistral AI, UiPath, Guardrails AI, and DraftLab. Total confirmed scope: 373 malicious package-version entries across 169 npm package names and 2 PyPI packages.

TeamPCP is not new to this pattern. StepSecurity had previously attributed to the same group the compromise of Aqua Security's Trivy scanner CLI (March 2026) and the Bitwarden CLI npm package (April 2026). TanStack was the third confirmed high-trust target in three months.

CISA added CVE-2026-45321 to the KEV catalog on May 27, 2026, citing confirmed active exploitation β€” not theoretical risk.

Why Provenance Attestation Did Not Help

This is the part that should recalibrate how security teams think about npm provenance.

Every malicious @tanstack/* version published on May 11 carries a valid Sigstore/SLSA Build Level 3 attestation. The attestation correctly states: this package was built by release.yml running on refs/heads/main in github.com/TanStack/router. That statement is true. The build was authorized by the pipeline. The pipeline had been injected with attacker code before the release step ran.

SLSA provenance answers the question: which pipeline produced this artifact? It does not answer: was that pipeline running authorized code? The gap between those two questions is exactly where this attack lived.

Provenance verification through npm audit signatures or Sigstore policy engines will pass on every compromised TanStack package. Do not treat a provenance badge as a safety signal for this class of attack.

Indicators of Compromise

Check your dependency trees, lockfiles, and CI logs for the following before concluding you are clean.

File indicators:
- router_init.js (~2.3 MB) at the root of any @tanstack/* package directory
- SHA-256 of malicious router_init.js: ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c
- setup.mjs at the root of any @tanstack/* package directory

lockfile indicators:

# Search package-lock.json or pnpm-lock.yaml for the malicious optionalDependencies entry
grep -r "79ac49ee" package-lock.json pnpm-lock.yaml yarn.lock 2>/dev/null
grep -r "tanstack/setup" package-lock.json pnpm-lock.yaml yarn.lock 2>/dev/null

Network indicators (exfiltration endpoints):
- filev2.getsession.org
- seed1.getsession.org
- seed2.getsession.org
- seed3.getsession.org

Cache key indicator (GitHub Actions):
- Linux-pnpm-store-6f9233a50def742c09fde54f56553d6b449a535adf87d4083690539f49ae4da11

Attacker GitHub identities:
- Commit author: claude <claude@users.noreply.github.com> (forged)
- Real accounts: zblgg, voicproducoes
- Payload commit: 79ac49eedf774dd4b0cfa308722bc463cfe5885c

Run a hash check across your installed node_modules:

find node_modules/@tanstack -name "router_init.js" -exec shasum -a 256 {} \;
# Any output at all is a red flag β€” this file should not exist in clean packages

What to Harden Now

The TanStack incident is a clean case study in how three individually known issues β€” a pull_request_target misconfiguration, cache poisoning, and OIDC token extraction β€” compose into a critical supply chain attack at scale. The defensive posture needs to operate at each layer.

Immediate Response if You Installed Affected Versions

If any system ran npm install and resolved a @tanstack/* version between 1.169.5 and 1.169.8 on May 11, 2026:

  • Rotate all credentials accessible from that machine: npm tokens, GitHub PATs and OIDC trust configurations, AWS static keys and instance role bindings, GCP service account keys, Kubernetes service account tokens, Vault tokens, SSH private keys.
  • Treat the install host as compromised until credentials are rotated and the host is reimaged or forensically cleared.
  • Audit GitHub Actions OIDC trusted-publisher bindings for unexpected additions β€” the worm establishes persistence by adding new trusts.
  • Check npm organization membership for unexpected maintainer additions.

Lockfile Hardening

Your package-lock.json or pnpm-lock.yaml is the first line of defense. Pin every @tanstack/* dependency to an exact version with integrity hash. The lockfile should make version drift impossible in CI.

// package.json β€” use exact versions, not caret ranges
{
  "dependencies": {
    "@tanstack/react-router": "1.169.9"
  }
}

Run npm ci instead of npm install in all CI pipelines. npm ci enforces exact lockfile resolution and fails if the lockfile is out of sync with package.json.

# CI install command β€” enforces lockfile, no drift
npm ci --ignore-scripts

# Add --ignore-scripts to block lifecycle hook execution
# This defeats the postinstall/prepare attack vector used in this incident

Disabling scripts globally is aggressive β€” some packages require them. Audit which packages in your tree actually need lifecycle scripts; @tanstack/* does not require postinstall scripts in normal operation.

Lockfile Integrity in CI

Add a lockfile integrity check as an early CI gate β€” before any install step runs:

# .github/workflows/ci.yml excerpt
- name: Verify lockfile integrity
  run: |
    git diff --exit-code package-lock.json pnpm-lock.yaml || \
      { echo "Lockfile modified β€” failing build"; exit 1; }

- name: Check for known-bad package entries
  run: |
    grep -r "79ac49ee\|tanstack/setup\|router_init" \
      package-lock.json pnpm-lock.yaml yarn.lock && \
      { echo "Malicious entry detected in lockfile"; exit 1; } || true

Dependency Version Monitoring

npm audit is not an early warning system. The advisory databases that feed it are populated with a delay of hours to days. The TanStack poisoned versions were live for at minimum 20 minutes before the first external detection, and Sonatype's automated scanner did not fully identify the scope until seven days later.

Subscribe to real-time scanning through tools that analyze new package versions as they are published β€” Socket.dev, Aikido Security, Endor Labs, or Snyk container scanning. These services flag suspicious patterns (obfuscated code, exfiltration network calls, unexpected optionalDependencies additions) before packages reach end users.

# One-time scan of your current tree against Socket advisory feed
npx @socketsecurity/cli scan --json | jq '.issues[] | select(.severity == "critical")'

GitHub Actions Hardening

The root cause of this incident was a pull_request_target workflow with insufficient isolation. Audit every workflow in your repositories:

# Dangerous pattern β€” runs with base-repo permissions for fork PRs
on:
  pull_request_target:

# Hardened pattern β€” isolate untrusted code explicitly
on:
  pull_request_target:
    types: [opened, synchronize, reopened]

jobs:
  build:
    # Only run with elevated permissions when the PR is from a repo owner
    if: github.event.pull_request.head.repo.full_name == github.repository

Pin all third-party Actions to commit SHAs, not tags:

# Unpinned β€” tag can be retargeted by upstream maintainer
- uses: actions/checkout@v4

# Pinned β€” immutable reference
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2

Purge Actions cache on every release workflow run and scope cache keys to branch names to prevent cross-fork cache poisoning:

- name: Restore pnpm store
  uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8  # v4.1.1
  with:
    path: ~/.local/share/pnpm/store
    # Scope key to branch β€” prevents cross-fork poisoning
    key: ${{ runner.os }}-pnpm-${{ github.ref_name }}-${{ hashFiles('pnpm-lock.yaml') }}
    restore-keys: |
      ${{ runner.os }}-pnpm-${{ github.ref_name }}-

npm Publish Pipeline Restrictions

If your team publishes packages to npm using OIDC trusted-publisher integration:

  • Restrict the OIDC trust to specific branches (e.g., refs/heads/main) and specific workflow files.
  • Add a manual approval environment gate before the publish step β€” this breaks fully automated worm propagation.
  • Require npm's publish access controls with 2FA even for OIDC-publishing accounts.
# Add environment gate to require manual approval before npm publish
jobs:
  publish:
    environment: npm-production  # Requires reviewer approval in GitHub settings
    steps:
      - run: npm publish --provenance

The Bigger Picture

The median time organizations take to update a vulnerable dependency sits around 278 days across supply chain research reports β€” nearly nine months from disclosure to patching. For a worm that spreads through the publish pipeline of every infected maintainer, that lag is an eternity. Every team that sat on an affected TanStack version became a potential vector for the next organization downstream.

High-trust CI pipelines with package-publish authority are the new primary target. Attackers no longer need to compromise an npm account directly β€” they compromise the workflow that holds transient publish authority, through a PR from an anonymous fork. The three techniques TeamPCP chained together β€” Pwn Request, cache poisoning, OIDC token extraction β€” are individually documented, individually mitigable, and individually present in open-source monorepo CI configurations everywhere.

TanStack moved fast: deprecated versions within the hour, hardened workflows the same week, published a detailed postmortem. The ecosystem response was credible. It cannot un-ring the bell for the 4,000+ installs before removal, or fix the fact that provenance attestation provided no protection at all.

Pin your lockfiles. Block install scripts in CI. Audit every pull_request_target workflow. Rotate credentials on any host that ran npm install on May 11. Do it this sprint, not this quarter.

Share:

Comments

0/1000

Related Articles