color npm package compromised
Thanks to my sponsors: Corey Alexander, Alex Krantz, Egor Ternovoi, Ross Williams, Victor Song, Anson VanDoren, Ryan, Nicolas Riebesel, Henrik Tudborg, avborhanian, Daniel Silverstone, Eugene Bulkin, Elnath, Andy F, Romain Kelifa, Jack Duvall, Blake Johnson, Raine Godmaire, Boris Dolgov, compwhizii and 253 more
On September 8 2025, around 13:00 UTC, someone compromised Josh Junon’s npm account (qix) and started publishing backdoored versions of his package.
Someone noticed and let Josh know:
Josh confirmed he’d gotten pwned by a fake 2FA (two-factor authentication) reset e-mail:
The phishing e-mail came from npmsj.help (registered 3 days prior) and claimed
users had to reset their 2FA:
The 2FA phishing e-mail
The raw email is available for the curious — it looks like it was sent via mailtrap (I’ve sent them an e-mail about it).
Over on Mastodon, Kevin Beaumont provided a list of affected packages:
And pointed out the scale of the attack: color alone has ~32 million weekly
downloads:
The payload
The complete payload is available on pastebin.
According to initial analysis, it appears it’s not meant to be running in a server environment, or on developers’ machines (in other words, not in nodejs/bun/etc.), but in the browser.
Which would mean that for the attack to be successful:
- Someone maintaining a crypto website/web-powered app would have to upgrade to the backdoored dependencies
- Those dependencies would have to be used on the front-end
- The crypto website would have had to be built, packaged, deployed
- Users of the website would’ve had to make transactions with the drainer active
In other terms, I think that if all people did was accept a PR that bumped some dependencies, and some tests ran in CI, then nothing bad has happened, yet. But people are still figuring out exactly what the payload is supposed to do, and all the affected packages.
De-obfuscating the payload through https://obf-io.deobfuscate.io/ yields good results, see this gist.
I went a step further and did a loose port to TypeScript to understand more of what’s going on.
In short, fetch and XMLHTTPRequest are hooked so that any crypto addresses
found in the response body that look like Bitcoin, Solana, Litecoin v2 etc. are
modified to be one of the many addresses controlled by the attacker.
Note that only the response body is modified, not the request body. Presumably… this targets API calls that would request which address to send funds to, and do the transfer through some other means?
Additionally, every 500ms up to 50 times, window.ethereum.request is called to see if
any Ethereum accounts have been authorized for use with Metamask.
If so, window.ethereum is monkey-patched to alter various transactions to go
attacker-controlled addresses.
In particular, it looks for:
approve(address,uint256)(0x095ea7b3) — replacing the destination & maxing out the amount- this codepath also logs the DEX name if known: Uniswap, PancakeSwap, 1inch, SushiSwap
permit(address,address,uint256,uint256,uint8,bytes32,bytes32)(0xd505accf) — replacing the destination & maxing out the valuetransfer(address,uint256)(0xa9059cbb) — replacing the destination but keeping the amounttransferFrom(address,address,uint256)(0x23b872dd) — replacing the destination but keeping the amount
There’s a Solana codepath as well, which changes various fields to 19111111111111111111111111111111, but
it’s unclear to me whether that would do anything successfully.
Current situation
Sep 8, 17:19 UTC
NPM has contacted Josh and told him they are working to the remove the packages.
See Josh’s timestamped comment
Sep 8, 17:11 UTC
- John is still locked out of his npm account
- chalk was patched by Sindre
- simple-swizzle is still compromised
see the obfuscated code starting with const _0x112fa8
The npm team seems rather unresponsive given the urgency of the situation:
It’s been almost two hours without a single email back from npm. I am sitting here struggling to figure out what to do to fix any of this. The packages that have Sindre as a co-publisher have been published over but even he isn’t able to yank the malicious versions AFAIU. If there’s any ideas on what I should be doing, I’m all ears.
The best place to stay informed is probably Kevin’s thread on Mastodon.
Did you know I also make videos? Check them out on PeerTube and also YouTube!
Here's another article just for you:
A dynamic linker murder mystery
I write a ton of articles about rust. And in those articles, the main focus is about writing Rust code that compiles. Once it compiles, well, we’re basically in the clear! Especially if it compiles to a single executable, that’s made up entirely of Rust code.
That works great for short tutorials, or one-off explorations.
Unfortunately, “in the real world”, our code often has to share the stage with other code. And Rust is great at that. Compiling Go code to a static library, for example, is relatively finnicky. It insists on being built with GCC (and no other compiler), and linked with GNU ld (and no other linker).