Vulnerability Analysis

CVE-2026-45321: How Attackers Hijacked 42 TanStack npm Packages Using GitHub Actions — And How to Protect Yourself

Executive Summary

On May 11, 2026, attackers published 84 malicious versions across 42 @tanstack/* npm packages in a six-minute window by chaining three separate GitHub Actions vulnerabilities: a pull_request_target Pwn Request, cross-boundary cache poisoning, and OIDC token extraction from runner memory. The attack — attributed to the cybercriminal group TeamPCP and named "Mini Shai-Hulud" by Snyk researchers — produced the first known npm supply-chain compromise bearing valid SLSA Build Level 3 attestations, meaning standard provenance checks would have passed. Any developer who ran npm install, pnpm install, or yarn install against an affected package version on that date should rotate every credential reachable from the install host immediately.


1. What Is This Vulnerability?

CVE-2026-45321 is a supply-chain attack that exploited three chained GitHub Actions weaknesses to publish credential-stealing malware under the trusted identity of the TanStack project. None of the three weaknesses alone was sufficient — the attack only succeeded because each one bridged a trust boundary the others assumed.

Attack Vector

Step 1 — The Pwn Request (cache poisoning phase)

The attacker created a fork of TanStack/router, deliberately renamed to zblgg/configuration to evade fork-list searches. They authored a malicious commit using the fabricated identity claude <claude@users.noreply.github.com> (no relation to Anthropic's AI) and embedded a ~30,000-line obfuscated JS payload inside packages/history/vite_setup.mjs. The commit used [skip ci] in the message to suppress CI on push.

The attacker then opened PR #7378 against TanStack/router#main. Because the repo contained a bundle-size.yml workflow using the pull_request_target trigger — which runs in the base repo's privileged context, not the fork's — the workflow automatically ran for this external PR without requiring maintainer approval. The critical flaw was that bundle-size.yml checked out the PR's merge ref (fork-controlled code) and executed it:

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  # ← fork code runs here

      - uses: TanStack/config/.github/setup@main  # transitively calls actions/cache@v5

      - run: pnpm nx run @benchmarks/bundle-size:build  # ← executes attacker's vite_setup.mjs

Step 2 — Cache Poisoning across the fork↔base trust boundary

When the benchmark job ran, actions/cache@v5's post-job hook saved the (now-poisoned) pnpm store to the cache key Linux-pnpm-store-${hashFiles('**/pnpm-lock.yaml')}. This cache entry is scoped to the base repository — not the fork — meaning it persists and is accessible to production workflows running on main. The attacker calculated the exact cache key the legitimate release.yml workflow would look up and wrote a poisoned pnpm store to that key.

After the cache was poisoned, the attacker force-pushed the PR back to the current main HEAD (a 0-file, no-op diff), then closed the PR and deleted the branch. The poisoned cache entry remained, invisible.

Step 3 — OIDC Token Extraction from Runner Memory

release.yml legitimately declares id-token: write to enable npm's OIDC trusted publishing. When a subsequent push to main triggered release.yml, it restored the poisoned cache. Attacker-controlled binaries now on disk were invoked during the build phase. Those binaries:

  1. Located the GitHub.Actions.Runner.Worker process via /proc/*/cmdline
  2. Read /proc/<pid>/maps and /proc/<pid>/mem to dump the runner's memory
  3. Extracted the OIDC token the runner mints lazily in memory when id-token: write is set
  4. Used that token to POST directly to registry.npmjs.org — bypassing the workflow's official Publish Packages step entirely

The malicious publish was authenticated with the exact same OIDC trusted-publisher binding TanStack legitimately uses, producing packages with valid SLSA Build Level 3 provenance attestations.

Step 4 — The Malware Payload

When any developer ran install against an affected version, npm resolved a malicious optionalDependencies entry, fetched an orphan payload commit from the fork network, and executed router_init.js (~2.3 MB, obfuscated) via npm's prepare lifecycle hook. The script:

  • Harvested credentials from: AWS IMDS/Secrets Manager, GCP metadata, Kubernetes service-account tokens, Vault tokens, ~/.npmrc, GitHub tokens (env, gh CLI, .git-credentials), and SSH private keys
  • Exfiltrated over the Session/Oxen encrypted messenger network (filev2.getsession.org, seed{1,2,3}.getsession.org) — no attacker-controlled C2, making IP/domain blocking the only network-level mitigation
  • Self-propagated: enumerated other packages the victim maintains via the npm registry API and republished them with the same injection — creating a worm that spread to @squawk/*, @mistralai/*, and other ecosystems

Real-World Impact

84 malicious package versions across 42 packages were live on npm for between 2 hours 53 minutes and 4 hours 35 minutes. The attack was detected externally by StepSecurity researcher ashishkurmi approximately 20 minutes after the initial publish. Because the malware ran at npm install time, any machine that installed an affected version must be treated as fully compromised. The self-propagating worm extended the blast radius beyond TanStack to other maintained packages in the npm ecosystem.

CISA added CVE-2026-45321 to its Known Exploited Vulnerabilities (KEV) catalog on May 27, 2026, with a remediation deadline of June 10, 2026 for federal agencies.


2. Who Is Affected?

Directly compromised packages — 42 packages from the TanStack/router monorepo (Router and Start packages only):

  • @tanstack/router, @tanstack/router-core, @tanstack/router-cli
  • @tanstack/start-* (all sub-packages)
  • @tanstack/history
  • All other packages in the TanStack/router monorepo

Confirmed safe — All other TanStack families are unaffected: @tanstack/query*, @tanstack/table*, @tanstack/form*, @tanstack/virtual*, @tanstack/store, @tanstack/devtools*.

At risk if you:

  • Ran npm install, pnpm install, or yarn install on May 11, 2026 (any time zone)
  • Have @tanstack/router or @tanstack/start-* packages and ran install that day
  • Used a CI/CD pipeline that resolved fresh dependencies on May 11, 2026
  • Installed from a lockfile that pinned to the affected version range (see Affected Versions in the GitHub Security Advisory GHSA-g7cv-rxg3-hmpx)

Secondary exposure — Any package maintained by someone who ran npm install against an affected version (via worm self-propagation).


3. How to Detect It (Testing)

Manual Testing Steps

Step 1: Check your npm install logs for the affected date

# Check npm cache entries from May 11, 2026
ls -la ~/.npm/_cacache/ | grep "2026-05-11"

# Check pnpm cache
ls -la ~/.local/share/pnpm/store/v3/

Step 2: Scan installed packages for the malicious optionalDependency fingerprint

# In any project directory, scan all installed package.json files
find node_modules -name "package.json" -not -path "*/node_modules/*/node_modules/*" | \
  xargs grep -l '"@tanstack/setup"' 2>/dev/null

# Look for the malicious GitHub commit reference specifically
find node_modules -name "package.json" | \
  xargs grep -l "79ac49eedf774dd4b0cfa308722bc463cfe5885c" 2>/dev/null

Step 3: Check for the malicious router_init.js payload file

find node_modules -name "router_init.js" -size +2M 2>/dev/null

# Check for the specific payload in any @tanstack package
find node_modules/@tanstack -name "router_init.js" 2>/dev/null

Step 4: Check network connections for exfiltration indicators

# Check for connections to Session/Oxen exfiltration network
grep -r "getsession.org\|filev2.getsession\|seed1.getsession\|seed2.getsession\|seed3.getsession" \
  ~/.npm/ node_modules/ 2>/dev/null

# Check DNS resolution history (macOS)
log show --predicate 'process == "mDNSResponder"' --last 30d 2>/dev/null | \
  grep "getsession.org"

Step 5: Verify installed package versions against safe versions

# List all @tanstack package versions installed
npm ls | grep "@tanstack"
# or
cat package-lock.json | python3 -c "
import sys, json
lock = json.load(sys.stdin)
tanstack = {k: v['version'] for k, v in lock.get('packages', {}).items() if '@tanstack' in k}
for k, v in tanstack.items():
    print(f'{k}: {v}')
"

Step 6: Cross-reference against the known malicious IOC

Malicious optionalDependencies fingerprint:
  "@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c"

Malicious file: router_init.js (~2.3 MB, not in "files" field of package.json)
Malicious cache key: Linux-pnpm-store-6f9233a50def742c09fde54f56553d6b449a535adf87d4083690539f49ae4da11
2nd-stage payload URLs: https://litter.catbox.moe/h8nc9u.js, https://litter.catbox.moe/7rrc6l.mjs
Exfiltration endpoints: filev2.getsession.org, seed{1,2,3}.getsession.org

Automated Scanning

Tool: Socket.dev Socket monitors npm package manifests for malicious optionalDependencies and behavioral anomalies. Run a scan of your dependency tree:

npm install -g @socketsecurity/cli
socket scan .

Expected output: Flags the @tanstack/setup github dependency pattern and the router_init.js file anomaly.

Tool: Snyk

npm install -g snyk
snyk test --all-projects

Expected output: CVE-2026-45321 flagged on any affected @tanstack/router version.

Tool: Grype (Anchore)

grype dir:node_modules --fail-on critical

Expected output: Reports GHSA-g7cv-rxg3-hmpx if affected packages are present.

Tool: StepSecurity Harden-Runner For CI/CD pipeline scanning, install StepSecurity's Harden-Runner in GitHub Actions workflows. It was the tool that detected the original attack within 20 minutes:

- uses: step-security/harden-runner@v3
  with:
    egress-policy: audit
    allowed-endpoints: >
      registry.npmjs.org:443
      # ... your allowed endpoints

Code Review Checklist for Your GitHub Actions Workflows

  • Search all workflows for pull_request_target trigger: grep -r "pull_request_target" .github/workflows/
  • Verify no pull_request_target workflow checks out fork code with ref: refs/pull/*/merge
  • Confirm actions/cache is not used in pull_request_target jobs that execute untrusted code
  • Check that id-token: write permission is scoped only to the specific job that needs it, not the entire workflow
  • Verify all third-party Actions are pinned to full SHA hashes, not floating tags like @v3 or @main
  • Run: grep -r "id-token: write" .github/workflows/ and audit every match

4. How to Fix It (Mitigation)

Step-by-Step Remediation

If you installed affected packages on May 11, 2026:

  1. Treat the install host as fully compromised. This is not optional — the malware ran at install time.

  2. Immediately rotate all credentials reachable from the affected machine:

    • AWS: Rotate IAM access keys, invalidate any temporary credentials from IMDS
    • GCP: Rotate service account keys, revoke OAuth tokens
    • Kubernetes: Rotate service-account tokens, check RBAC for damage
    • Vault: Revoke all tokens from the affected host
    • GitHub: Rotate personal access tokens, OAuth tokens, and SSH keys
    • npm: Revoke npm auth tokens (npm token revoke <token>)
    • SSH: Generate new key pairs; remove the old public key from all authorized_keys
  3. Revoke and re-issue any code-signing certificates accessible from the machine.

  4. Update to safe package versions immediately:

    # Update all @tanstack/router family packages
    npm install @tanstack/router@latest @tanstack/start@latest
    # or with pnpm
    pnpm update "@tanstack/*"
    

    All currently published versions of TanStack Router/Start are clean (TanStack issued an all-clear on May 15, 2026).

  5. Audit your own npm packages if you are a maintainer. Check whether the worm published infected versions of packages you maintain:

    npm view <your-package-name> versions --json | tail -20
    

    Compare publish dates; any publish on May 11–12, 2026 you don't recognize should be treated as a worm propagation.

  6. Clear all npm, pnpm, and yarn caches on affected machines:

    npm cache clean --force
    pnpm store prune
    yarn cache clean
    
  7. Clear GitHub Actions cache for any repositories that ran workflows on May 11, 2026 with @tanstack/router dependencies:

    gh cache list --repo <your-org>/<your-repo>
    gh cache delete --all --repo <your-org>/<your-repo>
    

Hardening Your GitHub Actions Workflows (Prevention)

Fix 1: Never execute fork code in pull_request_target context

# VULNERABLE — do not do this
on:
  pull_request_target:
jobs:
  build:
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}  # ← fork code in privileged context

# SAFE — split into two workflows
# Workflow 1: runs on pull_request (unprivileged)
on:
  pull_request:
jobs:
  build:
    steps:
      - uses: actions/checkout@v4  # checks out fork code, no privilege
      - run: pnpm build
      - uses: actions/upload-artifact@v4
        with:
          name: build-results-${{ github.event.number }}
          path: ./dist

# Workflow 2: runs on workflow_run (privileged, but only reads artifact)
on:
  workflow_run:
    workflows: ["Build PR"]
    types: [completed]
jobs:
  post-results:
    if: github.event.workflow_run.conclusion == 'success'
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: build-results-${{ github.event.workflow_run.id }}
      # Comment with results, no fork code executed here

Fix 2: Scope id-token: write to the specific publish job only

# VULNERABLE — entire workflow has id-token: write
permissions:
  id-token: write
  contents: read

# SAFE — only the publish job gets the permission
jobs:
  test:
    permissions:
      contents: read  # no id-token here
    steps: [...]

  publish:
    needs: test
    permissions:
      id-token: write  # only here, only when needed
      contents: read
    steps:
      - uses: actions/setup-node@v4
        with:
          registry-url: 'https://registry.npmjs.org'
      - run: npm publish --provenance

Fix 3: Pin all Actions to full SHA hashes

# VULNERABLE — floating tags
- uses: actions/checkout@v6.0.2
- uses: actions/cache@v5

# SAFE — pinned to commit SHA
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v6.0.2
- uses: actions/cache@5a3ec84eff668545d3b9f69bf1de44a5b7e1aa6d  # v5.0.2

Fix 4: Add repository_owner guard to all pull_request_target workflows

jobs:
  benchmark-pr:
    # Only run if the PR comes from within the same org/owner
    if: github.event.pull_request.head.repo.owner.login == github.repository_owner
    steps: [...]

Configuration Hardening

Enable these npm security settings in your .npmrc:

# Disable lifecycle scripts from untrusted packages (breaks some packages — test first)
ignore-scripts=false  # keep this false for most uses, but audit what scripts run

# Require package lock to prevent resolution drift
package-lock=true

# Enable audit on every install
audit=true

For organizations: enforce OIDC trusted publishing with npm's publish provenance and monitor publish events via npm's org audit log.


5. How to Test the Fix (Validation)

Regression Test Scenarios

  • Scenario A: Verify the malicious optionalDependencies fingerprint no longer exists in any installed package
  • Scenario B: Confirm npm audit returns zero critical findings for CVE-2026-45321
  • Scenario C: Verify all @tanstack/* packages installed resolve to post-incident clean versions
  • Scenario D: Confirm no router_init.js file exists in node_modules/@tanstack/

Security Test Cases

Test Case 1: Verify no malicious packages installed

  • Precondition: Update to latest @tanstack/* versions and run npm install
  • Steps:
    find node_modules/@tanstack -name "router_init.js" 2>/dev/null
    grep -r "getsession.org" node_modules/ 2>/dev/null
    grep -r "79ac49eedf774dd4b0cfa308722bc463cfe5885c" node_modules/ 2>/dev/null
    
  • Expected Result: No output — all three commands return nothing

Test Case 2: Verify npm audit is clean

  • Precondition: Updated package.json with latest @tanstack/*
  • Steps:
    npm audit --audit-level=moderate
    
  • Expected Result: found 0 vulnerabilities or no mention of CVE-2026-45321 / GHSA-g7cv-rxg3-hmpx

Test Case 3: Verify GitHub Actions workflows are hardened

  • Precondition: Applied workflow fixes above
  • Steps:
    # Check for any remaining pull_request_target with checkout of fork refs
    grep -A 20 "pull_request_target" .github/workflows/*.yml | grep "refs/pull"
    
    # Check for floating Action refs
    grep -r "uses:.*@v[0-9]" .github/workflows/ | grep -v "#.*sha"
    
  • Expected Result: No output from either command

Test Case 4: Verify OIDC scope is restricted

  • Precondition: Updated workflows with job-level permission scoping
  • Steps: Review each workflow file and confirm id-token: write appears only under specific jobs.<job-id>.permissions, not at the top-level permissions: block
  • Expected Result: grep "id-token: write" .github/workflows/*.yml shows it only inside specific job blocks

Automated Tests

Add this to your CI pipeline as a supply-chain integrity gate:

#!/bin/bash
# supply-chain-check.sh — Run after npm install in CI
set -e

echo "=== Supply Chain Integrity Check ==="

# Check for known malicious IOC fingerprints
MALICIOUS_COMMIT="79ac49eedf774dd4b0cfa308722bc463cfe5885c"
EXFIL_DOMAIN="getsession.org"
MALICIOUS_FILE="router_init.js"

echo "Checking for malicious commit reference..."
if find node_modules -name "package.json" | xargs grep -l "$MALICIOUS_COMMIT" 2>/dev/null | grep -q .; then
  echo "FAIL: Malicious commit reference found in node_modules"
  exit 1
fi

echo "Checking for exfiltration domain references..."
if grep -r "$EXFIL_DOMAIN" node_modules/ 2>/dev/null | grep -q .; then
  echo "FAIL: Exfiltration domain reference found in node_modules"
  exit 1
fi

echo "Checking for unexpected large JS files in @tanstack packages..."
LARGE_FILES=$(find node_modules/@tanstack -name "*.js" -size +2M 2>/dev/null)
if [ -n "$LARGE_FILES" ]; then
  echo "WARN: Large JS files found in @tanstack: $LARGE_FILES"
  exit 1
fi

echo "=== All checks passed ==="

6. Prevention & Hardening

Best Practices

Practice 1: Audit all pull_request_target workflows immediately This is the most important action. Search every repository for this dangerous pattern and apply the fix described in Section 4. GitHub's Security Lab has documented this class of attack since 2021; any workflow still using the vulnerable pattern is a standing supply-chain risk.

Practice 2: Implement dependency pinning with lockfile integrity checks Always commit package-lock.json or pnpm-lock.yaml. In CI, use npm ci (not npm install) to enforce lockfile-exact installs. Consider using a tool like Renovate with automerge for dependency updates rather than allowing floating version ranges.

Practice 3: Enable npm publish provenance AND verify it SLSA attestations are only useful if you verify them. CVE-2026-45321 produced valid SLSA Level 3 attestations for malicious packages — because the malware minted a legitimate OIDC token. Provenance tells you where a package was built, not whether the build environment was clean. Pair provenance with behavioral analysis tools like Socket.dev or Snyk's real-time monitoring.

Practice 4: Restrict npm publish permissions with 2FA and granular tokens

# Create a publish-only, automation-scoped npm token (not a classic full-access token)
npm token create --cidr-whitelist=<your-ci-ip> --read-only=false

Enable 2FA enforcement on the npm organization: npm org set 2fa-enforcement <org> enforced

Practice 5: Use StepSecurity's Harden-Runner or equivalent egress monitoring in CI The original attack was detected by a StepSecurity researcher — the tool they use is publicly available. Adding egress monitoring to your GitHub Actions workflows would have caught the exfiltration to getsession.org immediately.

Monitoring & Detection

Set up alerts for anomalous npm publish activity from your organization:

# Monitor your npm org's publish audit log (requires org admin)
npm audit --json | jq '.advisories | to_entries[] | select(.value.severity == "critical")'

# GitHub: Watch for unexpected workflow runs on protected branches
gh api repos/{owner}/{repo}/actions/runs --jq \
  '.workflow_runs[] | select(.conclusion == "failure") | {id, name, created_at}'

DNS/Network monitoring rules to add:

# Block/alert on connections to known Mini Shai-Hulud exfiltration endpoints
ALERT: dns query matches *.getsession.org
ALERT: dns query matches filev2.getsession.org
ALERT: http connection to litter.catbox.moe from build agent

GitHub Advanced Security alert rules:

If you use GitHub Advanced Security (GHAS), enable secret scanning push protection and code scanning with the pull_request_target misuse query from CodeQL's Actions security suite.


References

Latest from the blog

See all →