TanStack Backdoor Worm: How GitHub Actions Cache Poisoning Bypassed SLSA & Infected 47+ npm Packages – Full Mitigation Guide + Video

Listen to this Post

Featured Image

Introduction

A sophisticated supply chain worm leveraging GitHub Actions cache poisoning has compromised over 47 npm packages, including @tanstack/react-router and @uipath packages, with valid SLSA attestations that passed every integrity check. The attack exploited the `pull_request_target` trigger, a post-checkout script clobber technique identical to the cacheract vulnerability, and shared cache namespaces between a privileged fork workflow and the release pipeline, allowing backdoored code to ship to production with zero interaction from maintainers.

Learning Objectives

  • Understand the complete attack chain: entry via pull_request_target, pivot via post-checkout script overwrite, and cache poisoning that bypassed SLSA provenance.
  • Learn to detect similar supply chain risks in your GitHub Actions pipelines using audit commands and workflow analysis.
  • Implement defensive controls: cache isolation, least privilege for id-token: write, and post-step monitoring.

You Should Know

  1. Auditing Compromised Packages & Cleaning Your npm Cache

The malicious versions are: @tanstack/ versions 1.166.12, 1.166.15, 1.169.5, 1.169.8, plus packages under @uipath, @draftauth/client, and @draftlab/auth. To check if you are affected:

Linux / macOS / Windows (Git Bash or PowerShell):

 List installed TanStack packages and their versions
npm list @tanstack/react-router @tanstack/router-core @tanstack/history --depth=0

Audit for known malicious versions using npm audit (if reported)
npm audit --json | jq '.advisories | to_entries[] | select(.value.module_name | contains("tanstack"))'

Clean npm cache entirely (cross-platform)
npm cache clean --force

Remove node_modules and package-lock.json, then reinstall from scratch
rm -rf node_modules package-lock.json  Linux/macOS
 Windows PowerShell:
 Remove-Item -Recurse -Force node_modules, package-lock.json

npm install

Step‑by‑step guide:

1. Run `npm list` to identify vulnerable versions.

  1. If any match, immediately rotate any secrets or tokens that may have been exposed (attackers could exfiltrate runtime tokens).
  2. Clean cache to prevent restoration of poisoned artifacts.
  3. Rebuild from a trusted lockfile or pin to a known good version (e.g., npm install @tanstack/[email protected]).
  4. Audit your CI logs for unexpected outbound connections around the time of the package’s installation.

2. Detecting `pull_request_target` Abuse in GitHub Actions

The attack used `pull_request_target` – a trigger that executes workflow code from a fork but with the base repository’s secrets. Detect similar misconfigurations:

List all workflows using `pull_request_target` (Linux/macOS with GitHub CLI):

gh api repos/:owner/:repo/actions/workflows --paginate | jq '.workflows[] | select(.path | test(".")) | {name: .name, path: .path}'
 Then inspect each workflow YAML:
gh api repos/:owner/:repo/contents/.github/workflows/<workflow-file> --raw | grep -A5 "pull_request_target"

Windows (PowerShell with gh cli):

gh api repos/:owner/:repo/actions/workflows --paginate | ConvertFrom-Json | Select-Object -ExpandProperty workflows | ForEach-Object { $_.path }
 Download and search for pull_request_target

Step‑by‑step guide:

1. Identify all workflows using `pull_request_target`.

  1. Check if they run `actions/checkout` with `ref: ${{ github.event.pull_request.head.sha }}` or similar – this allows fork-controlled code to run.
  2. Verify that these workflows do not share a cache namespace with any release pipeline (see Section 3).
  3. If found, either rewrite to use `pull_request` (without secrets) or isolate caches using unique keys that include the PR number.

  4. Securing GitHub Actions Cache Namespaces Against Cross-Workflow Poisoning

The root cause was a shared cache key (setup-node-Linux-npm-…) between `bundle-size.yml` (triggered by pull_request_target) and `release.yml` (triggered on push to main). To prevent this:

Modify your cache key to be workflow‑ and branch‑specific:

- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-${{ github.workflow }}-${{ github.ref }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-${{ github.workflow }}-${{ github.ref }}-

Step‑by‑step hardening:

  1. List all cache keys used in your repository: search for `actions/cache` or `pnpm/action-setup` caches.
  2. Ensure that caches populated by `pull_request_target` or any fork‑accessible workflow are never restored by a release workflow.
  3. Use `github.workflow` and `github.ref` in the cache key as shown above.
  4. For release pipelines, set `cache-read-only: true` when restoring caches (pnpm v8+ supports --read-only-cache).
  5. Consider using ephemeral caches with `actions/cache/restore` and `actions/cache/save` separately, and never save from a pull request.

4. Hardening Release Pipelines with `id-token: write` Restrictions

The compromised release pipeline had `id-token: write` (to generate OIDC tokens for npm publishing). Attackers leveraged this to publish malicious packages with valid SLSA signatures. Mitigate:

Restrict `id-token` to only the publish job, and enforce environment protections:

jobs:
release:
permissions:
id-token: write  Only if absolutely needed
contents: read
environment:
name: npm-release
url: https://npmjs.com/package/your-package
steps:
- name: Restore cache (read-only)
run: pnpm install --frozen-lockfile --read-only-cache
- name: Publish to npm
run: npm publish --provenance

Step‑by‑step guide:

  1. Audit all workflows that have id-token: write. Is each one truly necessary? Remove from any workflow that can be triggered by a fork or pull request.
  2. Require a protected environment (e.g., npm-release) with required reviewers and branch restrictions.
  3. Use `–read-only-cache` during release jobs to prevent writing back poisoned data.
  4. Verify SLSA provenance after publish using `slsa-verifier` – but note that provenance only proves where it was built, not that the build environment was clean (complement with reproducible builds).

  5. Beyond SLSA: Validating Build Integrity with Reproducible Builds and Binary Comparison

SLSA attestations were valid because the build pipeline itself was compromised before signing. To catch this class of attack:

Compare your locally built package against the published one:

 Download the published tarball
npm pack @tanstack/[email protected] -o published.tgz
tar -xzf published.tgz -C published

Build the same version from source (assuming you have the commit)
git clone https://github.com/TanStack/router.git
git checkout v1.169.8
npm install
npm run build
npm pack -o local.tgz
tar -xzf local.tgz -C local

Diff recursively
diff -ur published/package local/package

Step‑by‑step guide:

  1. For critical dependencies, automate a reproducibility check in CI that builds from source and compares hashes with the registry package.
  2. If differences are found, investigate immediately – they may indicate backdoor injection.
  3. Use `npm audit signatures` (npm v10+) to verify package signatures against the registry, but understand this does not detect build‑time injection.
  4. Combine with Software Bill of Materials (SBOM) and vulnerability scanners like snyk test --sbom.

6. Incident Response for npm Supply Chain Compromise

If you suspect your environment is compromised:

Immediate steps (Linux/macOS/Windows WSL):

 1. Freeze all automated deployments
gh workflow disable release

<ol>
<li>Revoke all GitHub tokens and npm tokens
npm token revoke <token-id>
gh auth refresh -h github.com -s repo,workflow</p></li>
<li><p>Enumerate all running containers/processes that use affected packages
docker ps --format "table {{.Names}}\t{{.Image}}" | grep -i tanstack
Windows: Get-Process | Where-Object {$_.Modules.ModuleName -like "tanstack"}</p></li>
<li><p>Capture outbound network connections for forensics
sudo netstat -tunap | grep ESTABLISHED  Linux
Windows: netstat -an | findstr "ESTABLISHED"

Step‑by‑step IR plan:

  1. Isolate affected build runners – take offline any GitHub Actions runner that restored the poisoned cache.
  2. Rotate any secrets present in the environment during the compromised job (OIDC tokens expire quickly, but any manually set secrets are at risk).
  3. Review npm audit logs for the malicious versions and trace back to which CI job published them.
  4. Notify downstream consumers if you published a compromised package.
  5. Rebuild all artifacts from a clean, verified source without using any caches.

What Undercode Say

  • Key Takeaway 1: Valid SLSA provenance does not guarantee a clean build environment – the pipeline itself was trustworthy, but the environment became untrustworthy before signing.
  • Key Takeaway 2: Shared cache namespaces between privileged fork workflows and release pipelines are a critical design flaw; always isolate caches by workflow, branch, and event type.
  • Key Takeaway 3: `pull_request_target` is a high‑risk trigger – treat any workflow using it as potentially compromised and never let it feed into release pipelines.

Analysis:

This incident reveals that signature‑based integrity checks are insufficient when the build system can be poisoned before the signature is applied. Attackers are increasingly targeting CI/CD caches because they persist across jobs and are often shared between unprivileged and privileged contexts. The 25-day window between detection and public disclosure (due to prioritization friction) highlights the urgent need for automated, scalable disclosure mechanisms. Open-source maintainers are overwhelmed by AI‑generated low‑quality reports, causing genuine critical findings to be deprioritized. Until the industry adopts zero‑trust build principles – ephemeral caches, read‑only restore for release pipelines, and mandatory code review for any workflow that touches `pull_request_target` – supply chain worms of this nature will remain effective.

Prediction

Within 12 months, attackers will automate the discovery of GitHub Actions repositories that share cache keys across `pull_request_target` and release workflows, leading to a wave of supply chain worms targeting npm, PyPI, and RubyGems. The security community will respond by introducing “cache namespacing by default” in CI systems and deprecating `pull_request_target` for any workflow that writes artifacts consumed by production pipelines. SLSA will be extended with a new “build environment provenance” attestation that captures the exact runner image, cache digests, and network egress controls. Organizations that fail to implement ephemeral, read‑only caches for release builds will remain vulnerable to similar backdoors, regardless of their signature verification maturity.

▶️ Related Video (70% Match):

🎯Let’s Practice For Free:

IT/Security Reporter URL:

Reported By: Roni Carta – Hackers Feeds
Extra Hub: Undercode MoN
Basic Verification: Pass ✅

🔐JOIN OUR CYBER WORLD [ CVE News • HackMonitor • UndercodeNews ]

💬 Whatsapp | 💬 Telegram

📢 Follow UndercodeTesting & Stay Tuned:

𝕏 formerly Twitter 🐦 | @ Threads | 🔗 Linkedin | 🦋BlueSky