
Shell script to detect TanStack npm supply chain attack indicators (CVE-2026-45321 / GHSA-g7cv-rxg3-hmpx)
Detect indicators of the TanStack npm supply-chain attack on a developer machine, a repository, or a CI runner. Bash script, Docker image, and GitHub Action — same engine, three entry points.
CVE-2026-45321 / GHSA-g7cv-rxg3-hmpx
On May 11 2026, attackers published 84 malicious versions across 42
@tanstack/* packages via GitHub Actions cache poisoning. The payload steals
GitHub tokens, npm tokens, AWS/GCP/Azure credentials, and installs a
dead-man's switch daemon that wipes ~/ the moment the stolen token is
revoked.
Pick the entry point that matches where you are.
TAG=v1.0.0
curl -fsSLO https://github.com/fabriziosalmi/tanstack-compromise-checker/releases/download/$TAG/check.sh
curl -fsSLO https://github.com/fabriziosalmi/tanstack-compromise-checker/releases/download/$TAG/check.sh.sha256
sha256sum -c check.sh.sha256
bash check.sh --online
docker run --rm -v "$PWD":/scan \
ghcr.io/fabriziosalmi/tanstack-compromise-checker:v1.0.0 \
/scan true fail tanstack-findings.json '' GHSA-g7cv-rxg3-hmpx
The image is multi-arch (linux/amd64, linux/arm64) and ships with build
provenance signed via GitHub OIDC. Verify before running:
gh attestation verify \
oci://ghcr.io/fabriziosalmi/tanstack-compromise-checker:v1.0.0 \
--repo fabriziosalmi/tanstack-compromise-checker
permissions: {}
jobs:
check:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: fabriziosalmi/tanstack-compromise-checker@v1
with:
scan-dir: .
online: 'true'
fail-on: fail
A complete hardened workflow lives at
.github/workflows/tanstack-check.yml
and is the same workflow this repository runs against itself.
With --online, the script also fetches the GHSA advisory at runtime so that
detection coverage tracks the advisory's live state instead of a hard-coded
list of versions.
Affected family: @tanstack/router, @tanstack/react-router,
@tanstack/history, @tanstack/start, @tanstack/eslint-plugin-router, and
related packages.
Clean families (not affected): @tanstack/react-query,
@tanstack/react-table, @tanstack/react-virtual, @tanstack/form,
@tanstack/store.
# Quick check (current user's home)
bash check.sh
# Online mode: query the GHSA advisory and npm registry publish-time
bash check.sh --online
# Scan a specific directory
bash check.sh --scan-dir /path/to/your/repos
# JSON output for CI/SIEM integration
bash check.sh --json > findings.json
# Suggest pin commands for affected repos (dry-run)
bash check.sh --scan-dir /path/to/repos --fix
# Provide an extended known-bad list
bash check.sh --bad-versions-file ./extra-bad-versions.txt
| Code | Meaning |
|---|---|
| 0 | Clean — no indicators within scanned scope |
| 1 | Warnings only — review recommended |
| 2 | Failures — compromise indicators present |
| 3 | Tool/usage error |
This repository defends itself against the same attack class it detects. Concretely:
uses: is pinned to a 40-character commit SHA, not a
floating tag.permissions: {} at the top level; jobs grant only
what they need.actions/checkout is invoked with persist-credentials: false so no
authenticated git remote is left in $GITHUB_WORKSPACE.step-security/harden-runner
is the first step of every workflow and monitors runner egress.main; results are uploaded as SARIF.gh attestation.check.sh.sha256 covering all installable artefacts.The full policy is in SECURITY.md.
Do not pipe curl | bash for any security tool, including this one. Always
download, verify checksum, inspect, then run.
━━ 1 / 8 DEAD-MAN'S SWITCH ARTEFACTS
✔ No dead-man's switch artefacts found
━━ 2 / 8 PERSISTENCE VECTORS
✔ No persistence vectors detected
━━ 3 / 8 TOKEN / CREDENTIAL EXPOSURE
✔ No credential exposure detected
━━ 4 / 8 NETWORK INDICATORS
✔ No node TCP connections currently established (or none observable)
━━ 5 / 8 REPO SCAN — package.json + lockfiles
ℹ Found 12 package.json + 8 lockfile(s)
✔ No compromised family declared in any package.json
✔ No compromised pins found in any lockfile
━━ 6 / 8 INSTALLED node_modules — direct version check
✔ No compromised packages in installed node_modules
━━ 7 / 8 GITHUB ACTIONS HARDENING HINTS
✔ No risky GitHub Actions patterns detected (in scanned scope)
━━ 8 / 8 MINI SHAI-HULUD PAYLOAD + AUXILIARY IOCs
✔ No Mini Shai-Hulud payload / auxiliary IOCs detected
━━ SUMMARY
Passed: 8 Warnings: 0 Failed: 0
Scope : /Users/alice/dev
✔ No indicators of compromise within scope: /Users/alice/dev
Note: a clean result for /Users/alice/dev does not certify the rest of the system.
Do NOT revoke your GitHub token before stopping the daemon — the dead-man's switch will
rm -rf ~/the moment the token is revoked.
launchctl bootout gui/$UID ~/Library/LaunchAgents/com.user.gh-token-monitor.plistlaunchctl unload ~/Library/LaunchAgents/com.user.gh-token-monitor.plistsystemctl --user stop gh-token-monitor && systemctl --user disable gh-token-monitorrm -f ~/.local/bin/gh-token-monitor.sh~/.aws/credentials, ~/.kube/config, ~/.config/gh/hosts.yml,
~/.npmrc.The bash script needs:
bash 4+jq (recommended) — package-lock.json parsingpython3 (optional fallback) — used when jq is absentcurl — required for --onlinelsof or ss (optional) — network IOC heuristicThe Docker image bundles all of the above. Use it on systems where you can't or don't want to install these tools.
Issues and pull requests welcome. Note that this repository uses CODEOWNERS: all PRs require a maintainer review.
MIT.
| # | Check | Description |
|---|
| 1 | Dead-man's switch | ~/.local/bin/gh-token-monitor.sh, LaunchAgents + LaunchDaemons (macOS), systemd user + system units (Linux), running processes |
| 2 | Persistence | shell rc files, user/system crontab, /etc/cron.*, XDG autostart, global git core.hooksPath |
| 3 | Credentials | sensitive env vars, ~/.npmrc, ~/.yarnrc.yml, ~/.aws/credentials, ~/.config/gh/hosts.yml, ~/.netrc, ~/.docker/config.json, ~/.kube/config, .env* file enumeration |
| 4 | Network IOC | running node processes with established outbound TCP connections (heuristic, manual review) |
| 5 | Repo scan | package.json declared deps + lockfile pinned versions (package-lock.json, yarn.lock, pnpm-lock.yaml) |
| 6 | Installed | node_modules/@tanstack/*/package.json checked against the known-malicious version list, then against the npm registry time[<version>] when --online |
| 7 | Actions hints | .github/workflows/* scanned for pull_request_target, missing --ignore-scripts, actions/cache usage |
| 8 | Mini Shai-Hulud IOCs | payload files in node_modules (router_init.js, tanstack_runner.js), @tanstack/setup optionalDependency, AI-tool config tampering, C2 domains in source, attacker commit author + branch patterns, ransom-marked npm tokens, secondary worm-propagated packages, plus info-level heuristics for unknown payload shapes |
| Flag | Description |
|---|
--scan-dir DIR | Root directory to scan (default: $HOME). Refuses to scan system directories like /, /etc, /usr. |
--online | Query the GHSA advisory and the npm registry's time[<version>] to confirm compromise. Off by default — opt-in. |
--ghsa GHSA-ID | Override the advisory ID used in --online (default GHSA-g7cv-rxg3-hmpx). |
--attack-window-start ISO, --attack-window-end ISO | Override the publish-time window used in --online. |
--fix | Print recommended pin commands for affected repos (dry-run by default). |
--apply | With --fix: opt-in to execution. Currently still refuses blind update; manual pinning is required. |
--yes, -y | Skip confirmation prompt for --apply. |
--json | Emit findings as JSON to stdout (implies --quiet). |
--bad-versions-file F | Append entries (pkg@version, one per line) to the known-bad list. |
--no-color | Disable ANSI colors. |
--quiet, -q | Suppress per-check output, print only summary. |
-h, --help | Show help. |