chalk and debug get compromised, and 2 billion weekly downloads flinch
A phishing email to a maintainer, eighteen packages, and a crypto-stealing payload in the browser.
A maintainer of several extremely widely-used npm packages — including chalk, debug, ansi-styles, and strip-ansi — was phished, and malicious versions of eighteen packages were published.
Combined weekly download counts for the affected packages are on the order of two billion.
the phish#
An email from npmjs.help — a lookalike domain — claiming the account required two-factor re-verification, with a link to a credential harvesting page that also captured the TOTP code.
The maintainer has written publicly about it. The email was well-constructed, it arrived at a plausible time, and the domain was close enough to pass a quick glance.
This is worth saying clearly: the target was a competent, security-aware, long-time open source maintainer. Phishing that captures a TOTP in real time defeats the second factor. The control that actually stops this is a hardware security key or passkey, where the authentication is bound to the origin and a lookalike domain simply cannot complete it.
If you publish packages, use a passkey or hardware key. Today. TOTP is not sufficient against this attack and has not been for years.
the payload#
Browser-targeted, not server-targeted, which is unusual and clever.
The injected code hooked window.ethereum and intercepted fetch and XMLHttpRequest, watching for cryptocurrency transactions and swapping destination addresses for attacker-controlled ones — selecting a visually similar address to survive a casual glance at the confirmation dialog.
Because these packages are ubiquitous transitive dependencies of frontend build tooling, the payload had a plausible path into a very large number of shipped bundles.
Actual losses appear to have been small. Detection was fast — within a couple of hours — and most builds during the window did not pull the affected versions.
why the damage was limited#
Three things, in order:
- Lockfiles. Most production builds resolve to pinned versions. A new malicious release does not enter an existing lockfile without someone running an update.
- Speed of detection. The community noticed within hours. Someone diffed the published tarball against the repository and the mismatch was obvious.
- Payload specificity. Targeting crypto wallets is narrow. A payload targeting build-time credential theft would have done far more damage.
That third point should not be comforting. The same access with a better payload would have been much worse.
the controls, again#
The same short list as every one of these:
- Phishing-resistant auth on publishing accounts. Non-negotiable.
- Trusted publishing via OIDC rather than long-lived tokens.
- Lockfiles, committed, with exact versions in production.
--ignore-scriptsin CI.- A cooldown before adopting new versions.
None of these are new. All of them are still not universally adopted, which is the actual story.
the structural observation#
chalk adds color to terminal output. It is about a thousand lines. It has two billion weekly downloads because it is a transitive dependency of nearly everything in the JavaScript ecosystem.
A thousand-line utility maintained by a volunteer sits in the trusted computing base of a substantial fraction of the world's software, and the ecosystem's security posture depends on that person's email hygiene.
That is not a criticism of the maintainer, who did nothing unreasonable. It is a description of a system that has grown a dependency structure nobody designed and nobody can now change.
The fix is not "fewer dependencies." The fix is publishing infrastructure where a single compromised human credential cannot ship code to two billion machines. The registry has to make that impossible, because asking every maintainer to be unphishable has failed for a decade.
— Dom, September 9, 2025