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.
npmjs.help drove the maintainer to a page that captured credentials and a live TOTP codeThe 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.
npmjs.help - reads as a support subdomain, not a typoPretext: 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
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.
chalk, debug, ansi-stylesCombined 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
window.ethereum, silently mutated transaction destinations, and redirected funds to attacker-controlled walletsThe 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.
Mechanism: Hook
window.ethereum, mutate transaction destinations in flightEffect: 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
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.
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
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 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
🛡 How to Defend Against This Chain
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.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.