The Worm That Ate npm: How debug and chalk Became Weapons
A 2-hour window. 2.6 billion weekly downloads. The first self-propagating worm in npm history. How a phishing email against package maintainers put debug, chalk, and 16 other packages to work stealing crypto wallet addresses — and why your vibe-coded app was in the blast radius.
You ran npm install today. Did you check what you got?
Probably not. Nobody does. That's the whole point.
On September 8, 2025, attackers compromised 18 of the most widely-used packages in the npm ecosystem. Not obscure packages. Not weekend side projects. debug. chalk. Names that appear in the package.json of practically every Node.js application ever built. Combined, the affected packages were downloaded more than 2.6 billion times per week.
The malicious code was live for two hours before the community caught it. In those two hours, every project that pulled a fresh install got something extra. A payload that watched your crypto wallet transactions, swapped your destination addresses for the attacker's, and then — if the conditions were right — spread itself to every npm package you personally maintained.
That last part was new. In the history of npm supply chain attacks, nothing had done that before.
The packages you trust
Before getting into the attack, it helps to understand what these packages actually are.
debug is a tiny logging utility. You use it to print diagnostic messages during development. It has around 500 million weekly downloads — which makes it one of the most-installed packages in existence. It shows up as a dependency of Express, Mocha, and dozens of other foundational tools. Your app almost certainly has it, even if you never installed it directly.
chalk adds color to terminal output. It's what makes your build tools print green "success" and red "error" messages. Around 150 million weekly downloads. Same story: it's everywhere, often several levels deep in your dependency tree.
These packages are infrastructure. They're so ubiquitous that most developers have never thought about them as a security surface. They're just... there. Like the operating system or the runtime.
That invisibility is exactly what made them valuable targets.
How the attack started
The attackers didn't find a zero-day in npm's infrastructure. They didn't brute-force maintainer accounts. They sent emails.
On September 5, 2025, they registered a domain: npmjs.help. Convincing enough at a glance. Three days later, they sent phishing emails to the maintainers of 18 popular packages. The emails had a clear, urgent message: 2FA update required by September 10th. Failure to act would result in publish access being revoked.
Classic urgency trick. A deadline three days away. An action that sounds routine. A domain that looks plausible if you're reading fast.
Enough maintainers clicked, entered their credentials, and handed over their npm tokens. The attackers published malicious versions of 18 packages. The compromised releases landed in the registry at 13:16 UTC on September 8th.
What the malicious code actually did
The payload was clever in how it avoided detection.
The wallet-stealing code ran browser-side only. No server-side execution, no filesystem access during a Node.js install — just JavaScript that activated when a user's browser loaded a page. This kept it clean during CI/CD pipeline runs, where security tooling tends to look hardest.
When a user visited a page in a compromised app and initiated a crypto transaction, the malicious code intercepted the API call. It read the destination wallet address and ran it through a Levenshtein distance algorithm — a technique that measures the similarity between two strings — to find a lookalike address it controlled. Then it swapped the real address for the fake one.
The swap was designed to look legitimate. A Levenshtein-similar address shares most characters with the real one. In the fraction of a second before you confirm a transaction, you're unlikely to spot a difference in characters 28 through 34 of a 42-character Ethereum address.
Supported targets: Ethereum, Bitcoin, Solana, Tron, Litecoin, Bitcoin Cash. If you were building a payment interface or crypto dashboard with any of the affected packages in your dependency tree, your users' transactions were at risk.
The two-hour window
The attack timeline was tight:
| Time (UTC) | Event |
|---|---|
| Sep 5 | Attackers register npmjs.help phishing domain |
| Sep 8, 13:16 | Malicious package versions published to npm |
| Sep 8, ~15:20 | Community spots anomalies in package diffs |
| Sep 8, ~15:30 | Incident response begins, packages flagged |
| Sep 9 | CISA issues advisory: "Widespread Supply Chain Compromise Impacting npm Ecosystem" |
Two hours from publish to discovery. That's fast — genuinely impressive community response. Package diff monitoring tools, eagle-eyed maintainers, and npm's own security team all played a role.
But two hours is also a long time in npm terms. If your project ran npm install in that window — a fresh dependency pull, a CI build, a deploy — you got the infected version. No action required on your part. The infection was passive, automatic, and invisible.
Then came the worm
The wallet swap was the immediately harmful payload. But the more remarkable piece — the one that earned this attack its name — was what Palo Alto Unit 42 called the "Shai-Hulud" secondary payload.
If you haven't read Dune: the Shai-Hulud is the great sandworm of Arrakis. Ancient, enormous, and it moves through the sand in ways you don't see coming until it's too late.
The worm component did three things:
-
Scanned the victim's filesystem for stored credentials — GitHub tokens, npm auth tokens, AWS keys. Anywhere they might be sitting:
.npmrcfiles, shell history, environment configs, credential stores. -
Exfiltrated everything it found to attacker-controlled infrastructure.
-
Used the stolen npm token to publish malicious versions of every package the victim maintained.
Step three is what made it novel. Previous supply chain attacks compromised packages by targeting their maintainers. Shai-Hulud turned each victim into a new attacker. If you maintained debug, and you ran npm install with an infected package in your dependency tree, the worm could steal your npm credentials and publish infected versions of everything you owned. Those packages would then infect their own downstream maintainers, who would infect theirs.
Self-propagating. First of its kind in npm. The security community had theorized about this attack pattern for years. September 8th was the first confirmed real-world execution.
Shai-Hulud 2.0 and 3.0
The attackers didn't stop after the initial incident.
November 2025 — Shai-Hulud 2.0: Palo Alto Unit 42 tracked a second wave involving more than 25,000 malicious GitHub repositories, operated by approximately 350 accounts. The worm had evolved. Alongside credential theft, 2.0 added a "scorched earth" fallback: if it couldn't find credentials worth stealing, it wiped the victim's entire home directory. No crypto heist opportunity? Fine — just destroy the machine instead.
December 29, 2025 — Shai-Hulud 3.0: A third iteration with multi-platform evasion techniques, designed to slip past the detection signatures that had been written for earlier variants. The specifics of the evasion methods were documented by Unit 42 but the core pattern remained the same: get in through a dependency, find credentials, spread.
Three versions in four months. The attackers were iterating on their tooling the same way any software team iterates on a product.
Why AI-generated apps were in the crosshairs
Here's where this connects directly to how you probably built your app.
When an AI tool generates a package.json for you — whether it's Cursor writing your backend, Lovable scaffolding your full-stack app, or Claude setting up a new project — it uses caret ranges by default:
{
"dependencies": {
"debug": "^4.3.4",
"chalk": "^5.3.0",
"express": "^4.18.2"
}
}
That ^ prefix means "this version or any compatible newer version." When you run npm install, npm fetches whatever the current latest compatible release is. If the latest release was published 90 minutes ago by an attacker who phished the maintainer, you get that version.
Pinned versions look different:
{
"dependencies": {
"debug": "4.3.4",
"chalk": "5.3.0",
"express": "4.18.2"
}
}
No caret. Exact version. npm installs exactly what you specify — no surprises.
The AI tools aren't doing anything wrong here. Caret ranges are the community standard. They're what npm recommends, what tutorials teach, what the ecosystem expects. But they mean that if a package you depend on gets compromised, your next install picks it up automatically.
There are also a few compounding factors that hit vibe-coded apps harder than traditional projects:
- Lockfile hygiene is often skipped.
package-lock.jsonoryarn.lockpins your entire dependency tree at install time. But they only protect you if you commit them and runnpm ciinstead ofnpm install. Fast-moving projects often skip this or regenerate locks casually. - AI tools recommend whatever worked. If Claude or Cursor recommends
debugbecause it appears in a million training examples, it doesn't know those examples predate September 8th. The recommendation is sound — the package is legitimate — but the tool can't account for post-training events. - Dependency trees are deep. Your app might not directly install
debug. Butexpressdepends on it. So does your testing framework. So does your linter. You have hundreds of transitive dependencies you've never thought about, any one of which can become a vector.
What to check right now
You can take meaningful action in about 15 minutes.
| Check | How to do it | Why it matters |
|---|---|---|
| Audit your lockfile | Run npm audit or yarn audit | Flags known compromised versions |
| Check install dates | Review npm install logs from Sep 8–9, 2025 | Identify if you pulled infected packages |
| Pin critical deps | Remove ^ from direct dependencies in package.json | Prevents automatic uptake of new versions |
Use npm ci in CI | Replace npm install with in your pipeline |
If you maintain any npm packages yourself, check whether any unexpected versions were published between September 8–10, 2025. Log into npmjs.com, go to your packages, and look at the version history. If there's a version you didn't publish, treat it as a confirmed compromise and unpublish it immediately.
Going forward: lockfiles are not optional
The single highest-leverage habit change you can make after this incident is treating your lockfile as a security artifact.
Commit your lockfile. Always. package-lock.json and yarn.lock should be in version control, not in .gitignore. This gives you a cryptographic record of what you intended to install, and it lets you audit diffs when the file changes.
Run npm ci instead of npm install in automated environments. npm ci installs exactly what's in the lockfile. If the lockfile and package.json disagree, it fails loudly instead of silently updating.
Review lockfile diffs in PRs. When a PR updates a dependency, the lockfile diff shows every package that changed — including transitive ones. It's tedious, but it's the only way to catch unexpected additions. Tools like Socket.dev automate this and flag suspicious new packages.
Subscribe to security advisories for your direct dependencies. GitHub's Dependabot alerts, npm's advisory feed, or Socket's monitoring. You want to know within minutes, not hours, when a package in your tree is flagged.
# Enable npm audit in your CI pipeline
npm ci
npm audit --audit-level=high
# Fail the build if any high or critical issues are found
# This catches compromised packages once they're flagged in the advisory database
None of this would have stopped the initial two-hour window — the package wasn't in the advisory database yet. But running npm ci instead of npm install would have protected you the moment your lockfile was generated before the attack. And a lockfile diff review would have flagged the unexpected version bump.
This is solvable
The two-hour window is the most important number in this story — and not just because it's alarming.
The npm community detected and responded to a coordinated attack against 18 packages with 2.6 billion weekly downloads in under two hours. That's a functioning immune system. Package diff monitoring, community vigilance, and fast incident response worked. The attack was contained.
What that means for you: the ecosystem has gotten meaningfully better at detecting this kind of attack. The tooling exists. The processes exist. Your job is to take advantage of them.
Commit your lockfile. Run npm ci. Turn on npm audit in CI. Rotate your npm token if you were in the install window. Scan your app before the next deploy.
You built something real. A few minutes of dependency hygiene is what keeps it that way.
What to do before you ship
- Run
npm auditin your project directory right now. Zero effort, immediate signal. - Switch to
npm ciin any automated build or deploy pipeline. - Commit and review your lockfile. If it's in
.gitignore, remove it from there. - Rotate your npm credentials if you had any packages installed between September 8–9, 2025.
- Scan your app with Flowpatrol to check for exposed secrets and dependency-sourced issues that static analysis misses. Paste your URL, get a report.
Supply chain hygiene isn't a one-time fix. But it's also not complicated. These five steps take less time than the attack window that exposed 2.6 billion downloads worth of apps.
The September 2025 npm supply chain attack and Shai-Hulud worm were documented by Palo Alto Unit 42, Socket Security, and CISA's advisory "Widespread Supply Chain Compromise Impacting npm Ecosystem." Version escalations in November and December 2025 were tracked by Unit 42's threat intelligence team.