Cloud Security Office Hours Banner

npm debug / chalk 2025

Step-by-step kill chain mapped to MITRE ATT&CK Cloud, sourced from official post-mortems and primary technical analyses.

September 2025 Critical npm

npm debug / chalk - Fake 2FA-Reset Email → Live TOTP Relayed → Maintainer Account Taken Over → 18 Packages at 2.6 Billion Weekly Downloads Trojanized

On September 8, 2025 an attacker phished the npm account of the maintainer known as Qix using a convincing two-factor-reset email sent from the lookalike domain npmjs.help. The page captured a username, a password, and a live TOTP code, which was relayed in real time to take over the account. Eighteen packages were trojanized, including chalk, debug and ansi-styles - utilities so deeply embedded in the JavaScript ecosystem that together they account for over 2.6 billion downloads a week. The payload was narrow rather than greedy: browser-side cryptocurrency theft, hooking window.ethereum to silently rewrite transaction destinations. The maintainer acknowledged the compromise around 11:00 EST and the malicious versions were pulled within hours. A week later the same phishing pattern opened the Shai-Hulud worm, which is the reason this small, fast-contained incident belongs on the page.

2.6 billion weekly downloads across the affected packages
18 packages trojanized, including chalk, debug and ansi-styles
Hours from acknowledgement to removal
One week later: the same phishing pattern launched Shai-Hulud
📄 Aikido - npm debug and chalk packages compromised ↗ 📄 Wiz - Breaking down impact and scope ↗
🎣 Initial Access - A 2FA-Reset Email From npmjs.help
01
A convincing two-factor-reset notice sent from the lookalike domain npmjs.help drove the maintainer to a page that captured credentials and a live TOTP code
T1566.002 - Phishing: Spearphishing Link T1111 - Multi-Factor Authentication Interception

The domain choice is the craft here. npmjs.help is not a misspelling anyone would catch by squinting - it reads as a plausible support subdomain for a registry whose real home is npmjs.com, and support-flavoured domains are common enough across the industry to look unremarkable. The pretext was a required two-factor-authentication update, which is both routine and mildly urgent, and it makes entering a one-time code the obviously correct action rather than a red flag. The attacker collected the username, the password, and a live TOTP code, and relayed all three to the real npm in real time. This is not an MFA failure in the sense of MFA being absent. It is MFA working exactly as designed while being proxied, and it is the reason "we require MFA" and "we require phishing-resistant MFA" describe different security postures.

Lookalike domain: npmjs.help - reads as a support subdomain, not a typo
Pretext: A required two-factor-authentication update
Captured: Username, password, and a live TOTP code
Mechanism: Real-time relay to the genuine site inside the code's validity window
Control that breaks it: Origin-bound FIDO2 or passkeys, which will not authenticate to the wrong domain
npmjs.helpTOTP Relay2FA Reset PretextT1111
🔓 Account Takeover - One Maintainer, Eighteen Packages
02
With full account control the attacker published trojanized versions of eighteen packages, among them chalk, debug and ansi-styles
T1195.002 - Compromise Software Supply Chain T1078 - Valid Accounts

The blast radius here is a property of how the JavaScript ecosystem is shaped rather than anything the attacker engineered. chalk colours terminal output. debug prints debug messages. ansi-styles handles terminal escape codes. These are tiny, boring, extremely stable utilities that thousands of other packages depend on transitively, so almost nobody installs them deliberately and almost every JavaScript project has them. Together the affected set carries over 2.6 billion downloads a week. One maintainer's inbox therefore sat upstream of a substantial fraction of all JavaScript builds on Earth - and the maintainer of a widely depended-upon utility typically has none of the security budget, tooling, or support that their downstream consumers do.

Packages: 18 total, including chalk, debug, ansi-styles
Combined reach: Over 2.6 billion downloads per week
Why so deep: Tiny stable utilities pulled in transitively by thousands of packages
Structural asymmetry: Enormous downstream reach, essentially no upstream security budget
chalkdebugTransitive DependencyT1195.002
💰 Execution - A Crypto Drainer in the Browser
03
The injected code hooked window.ethereum, silently mutated transaction destinations, and redirected funds to attacker-controlled wallets
T1059.007 - JavaScript T1657 - Financial Theft

The payload was unusually disciplined. Rather than harvest credentials from build machines - the obvious play, and the one Shai-Hulud would run a week later - it targeted end users of applications built with the compromised packages. It intercepted browser wallet interactions through window.ethereum, rewrote the destination address on transactions in flight, and sent funds to the attacker. The victim approves a transfer that looks correct in their own interface and the money goes elsewhere. Note where the harm lands: not on the maintainer, and not on the developers who installed the package, but on their users, several dependency hops away and with no visibility into any of it.

Target: End users of applications built with the affected packages
Mechanism: Hook window.ethereum, mutate transaction destinations in flight
Effect: The user approves a correct-looking transfer; funds go to the attacker
Not targeted: Build-machine credentials - unusually restrained for this class of compromise
Where the harm lands: Several dependency hops downstream, on people with no visibility
Crypto Drainerwindow.ethereumClient-Side PayloadT1657
⏱ Response - Hours, Not Days
04
The maintainer acknowledged the compromise around 11:00 EST and the malicious versions were removed from npm within hours

Containment was genuinely fast, and given the download numbers that speed is the reason this did not become a far larger incident. The maintainer publicly acknowledged the phishing, npm pulled the malicious versions the same day, and platforms including Vercel published response guidance for customers within a day. The limiting factor on damage was how quickly a single volunteer noticed an email had been fake and said so publicly - which is worth stating plainly, because it is not a control anyone designed, and it is not one that can be relied upon. The ecosystem got a good outcome here from an individual's fast, transparent response rather than from any structural protection.

~11:00 EST, September 8: Maintainer acknowledges the compromise publicly
Same day: Malicious versions removed from npm
Within a day: Platform response guidance published for downstream consumers
What actually limited the damage: One volunteer noticing quickly and disclosing openly
Uncomfortable corollary: That is not a control, and it cannot be depended on
Fast ContainmentTransparent DisclosureVolunteer Maintainer
🔁 Aftermath - The Same Playbook, One Week Later
05
A week after this incident the same maintainer-phishing pattern opened the Shai-Hulud worm, which added self-replication and did not stay contained
T1195 - Supply Chain Compromise

This is why a fast-contained incident earns a full write-up. On its own, debug/chalk is a story about eighteen packages, a crypto drainer, and a good response. Read next to what followed on September 15, it is a proof of concept. The Shai-Hulud worm used the same entry - phishing maintainers with npm-branded MFA pretexts - and changed one thing: instead of manually publishing a payload, it automated republication into every package the compromised maintainer owned, and then into every maintainer infected downstream. Same door, self-replicating body, 500+ packages instead of 18. The technique was demonstrated in public and industrialized within a week, which is a useful calibration on how quickly successful tradecraft is copied in this ecosystem.

September 8: debug/chalk - manual publication, 18 packages, contained in hours
September 15: Shai-Hulud - same phishing entry, self-replicating, 500+ packages
What changed: Automation of the republication step
Calibration: One week from public demonstration to industrialized worm
PrecursorShai-HuludRapid IterationT1195

🛡 How to Defend Against This Chain

Phishing-resistant MFA on maintainer accounts, because TOTP was relayed in real time. The account had two-factor authentication and it did not help, since a proxied one-time code is a working credential. FIDO2 keys and passkeys are bound to the origin and simply will not authenticate to npmjs.help. If you maintain a package anyone depends on, this is the highest-leverage change available to you, and registry-level enforcement for high-download packages is the ecosystem-level version of the same fix.
Pin dependencies by hash and add a publication delay. Malicious versions existed for hours. A lockfile with enforced integrity hashes means a new version cannot silently enter your build at all, and a policy of not installing anything published within the last 24 to 48 hours would have skipped this entire window. Between them these two controls neutralize most short-lived registry compromises without requiring you to know anything about the specific attack.
Watch for suspicious version churn in utility packages you never think about. Nobody installs ansi-styles on purpose, which is exactly why a surprise release of it deserves attention. Alert on new versions of transitive dependencies that are unusually small, unusually stable historically, or unusually deep in your tree - and on package size jumping between patch versions, which is a cheap signal that catches injected payloads across several of the chains on this site.
Remember that the payload can target your users rather than your build. Most dependency-compromise response focuses inward: rotate CI credentials, check the build machines. This payload never touched them - it shipped to production and stole from end users in their browsers. Content Security Policy and Subresource Integrity constrain what injected client-side code can do, and any application handling payments or wallets should treat its dependency tree as part of its payment security boundary.
Support the maintainers you depend on, because the asymmetry is the root cause. A single volunteer's inbox sat upstream of 2.6 billion weekly downloads with none of the security tooling their consumers take for granted. Fund the projects in your critical path, contribute maintenance effort, and support registry-level protections for high-impact packages. This is the structural fix; everything else on this list is mitigation.
Assume any successful technique will be industrialized within about a week. This compromise was contained quickly and well, and seven days later the same entry vector reappeared with self-replication attached and reached 500+ packages. When you see a novel technique work anywhere in your ecosystem, the planning assumption should be that a worse version is imminent - so respond to the class of attack, not the specific incident.

Related defense topics