← Blog
·10 min read

The npm Attack That Poisoned 2.6 Billion Weekly Downloads in Two Hours

In September 2025, attackers phished a single npm maintainer and pushed malicious code into chalk, debug, and 16 other packages. The payload silently rewrote crypto wallet transactions. It was live for two hours.

Yumi Hirasako

Technical Writer

They didn't attack your code.

They attacked the person who maintains the package you use to print colored text in your terminal.

On September 8, 2025, at 9:00 AM EST, a threat actor sent a phishing email to Josh Junon — username qix on npm, maintainer of some of the most foundational utility packages in the JavaScript ecosystem. The fake domain looked close enough to the real npm support site. He clicked. His credentials were captured.

By 13:16 UTC, the attacker had published malicious new versions of 18 npm packages. By 15:30 UTC, they were removed. Two hours.

In that window, code designed to silently intercept and redirect cryptocurrency transactions was live inside chalk, debug, ansi-styles, strip-ansi, and 14 other packages that collectively see 2.6 billion downloads per week.


What chalk and debug actually are

Before getting into what happened, it's worth understanding why these packages matter.

debug is a lightweight logging utility. It's used everywhere — Express, Socket.io, Mongoose, Mocha, Webpack, countless other tools depend on it transitively. You probably have it in your node_modules right now even if you've never installed it directly.

chalk is for terminal string styling. It colors your console output. Again — used by almost every CLI tool, build system, and dev server in the Node.js world.

Neither package does anything remotely related to cryptocurrency. That's the point. An attacker targeting a crypto-stealing payload doesn't go after a niche crypto library. They go after something that's already installed everywhere, in every kind of project.


How the attack unfolded

Step 1 — The phishing.

The attacker registered a domain that looked like official npm support: npmjs.help. They sent a message to the maintainer through this domain. The message appeared legitimate. Josh Junon entered his credentials.

Step 2 — Account takeover.

With access to the qix npm account, the attacker had publish rights to every package that account maintained. That included debug, chalk, ansi-styles, strip-ansi, supports-color, color-name, color-convert, color-string, and others. All foundational. All heavily depended upon.

Step 3 — Malicious publish.

The attacker published new patch versions of the compromised packages. The version bumps were small — the kind that would normally trigger an automatic update in any project not pinned to exact versions. The code inside looked like a normal update.

Step 4 — The payload activates.

The injected code was designed to run in the browser. It hooked into browser fetch and XMLHttpRequest calls, and into wallet APIs like window.ethereum and Solana's signing methods. When a user on an affected site initiated a crypto transaction, the payload intercepted it before signing and rewrote the destination address to one controlled by the attacker.

The user saw nothing different. The UI confirmed the correct address. The transaction went somewhere else.

Step 5 — Detection and removal.

Aikido Security's automated threat feed detected the anomalous publish activity at 13:16 UTC. Within hours, the npm security team had removed the malicious versions. The maintainer regained access and confirmed the compromise publicly.

Total time the malicious versions were available: roughly two hours.


Why two hours was enough

Before the attack, Wiz's telemetry showed that 99% of cloud environments had at least one instance of one of the targeted packages. After the malicious versions were published, 10% of cloud environments had pulled the malicious code into their builds.

That number isn't surprising when you understand how npm dependency resolution works.

Most projects don't pin to exact versions. They use ranges — ^5.3.0 means "anything compatible with 5.3.0." When you run npm install, npm resolves those ranges against what's currently live on the registry and pulls the latest matching version. If a malicious patch version is live at the moment your CI pipeline runs, that's what you get.

This is how a two-hour window translates to thousands of affected builds. CI pipelines run continuously. Every pull request, every deployment trigger, every scheduled build that ran during that window against a project without a committed lockfile resolved to the infected versions.


What "transitive dependency" means for you

Here's the part most developers don't think about until something like this happens.

You might not have debug or chalk in your own package.json. You might never have typed npm install chalk. But if you installed Express, or Webpack, or Jest, or a dozen other common packages — you have debug and chalk in your project. They're transitive dependencies: dependencies of your dependencies.

The attack surface for a compromised foundational package isn't the people who install it directly. It's everyone who installs anything that depends on it.

# See how many of your packages depend on debug transitively
npm ls debug
 
# Same for chalk
npm ls chalk

Run that in any medium-sized Node.js project and you'll likely see a tree with dozens of entries. Each one of those is a path through which a malicious version could have entered your build.


The payload's actual behavior (and why it failed)

The injected code had a specific target: browser-side crypto transactions. It wrapped the native fetch and XMLHttpRequest functions, and patched wallet provider APIs, to intercept outgoing transaction data and swap recipient addresses.

The attack failed to cause significant financial damage for a reason that's almost ironic: the payload had a bug. The address-rewriting logic contained a coding error that prevented it from working correctly in most cases. The Ledger CTO confirmed minimal impact shortly after the incident.

That doesn't change what it was designed to do. If the payload had been correct, every crypto transaction initiated through a site whose client bundle included a transitive dependency on any of these 18 packages — during the two-hour window — would have been redirected.

The protection wasn't good security practices. It was a mistake in the attacker's code.


How to check if your builds were affected

The exposure window was 13:15–15:30 UTC on September 8, 2025.

Step 1 — Check your build timestamps.

Look at your CI/CD logs for any builds that ran during that window. Any build that ran npm install (not npm ci) without a pre-committed lockfile during those two hours is a candidate for exposure.

Step 2 — Check which versions you resolved.

The malicious versions included:

Package Malicious version
chalk 5.6.1
debug 4.4.2
ansi-styles 6.2.2
strip-ansi 7.1.1
supports-color 9.4.1
# In any project where you suspect exposure:
npm list chalk debug ansi-styles strip-ansi supports-color

If any of those version numbers appear in your resolved tree, and you have build artifacts from the window above, treat those artifacts as potentially compromised.

Step 3 — Assess your exposure.

If you were affected: the payload targeted browser-bundled code interacting with crypto wallets. If your app doesn't ship JavaScript to a browser, or doesn't interact with window.ethereum or Solana wallet APIs, the payload had no practical effect on your users.

The more serious concern is developer machine and CI/CD exposure — if the packages ran server-side with access to environment variables during the window, treat those credentials as potentially compromised and rotate them.


What this attack is actually telling you

The September 2025 npm incident wasn't a sophisticated technical exploit. There was no zero-day in npm's infrastructure. No cryptographic flaw was broken.

A maintainer got phished. That's all it took to compromise 2.6 billion weekly downloads.

The JavaScript ecosystem runs on a model of implicit trust. You install a package, and you're trusting not just the code in that package, but the security posture of every person who has publish rights to that package, and every maintainer of every transitive dependency, all the way down.

For most projects, that's hundreds of people you've never heard of, with no visibility into their security practices, and no control over whether their npm account has MFA enabled.

This isn't a reason to stop using open source. It's a reason to treat your dependency tree as an attack surface — because attackers already do.

DataHogo's scanner flags newly published versions of your dependencies that deviate from expected behavior patterns, catching supply chain anomalies before they hit your builds. Run a free scan.


Three things to do after reading this

1. Use npm ci in all CI/CD pipelines, not npm install.

npm ci installs from the lockfile exactly. It doesn't resolve ranges against the live registry. It doesn't pull new versions. If your lockfile was committed before the attack window, npm ci would have kept you clean. Make this the default in every pipeline you control.

2. Commit your lockfile and treat it as a security artifact.

Your package-lock.json or yarn.lock is not just a convenience file. It pins every dependency — direct and transitive — to a specific version and hash. Commit it. Review changes to it the same way you review code changes. A lockfile diff that shows 18 packages updating at once should raise a flag.

3. Enable npm package auditing in your pipeline.

npm audit --audit-level=high

This won't catch a zero-day supply chain compromise in real time, but it catches known malicious packages that have already been flagged. Run it as part of every build. Fail the build if it finds anything at high severity or above.


TL;DR

  • On September 8, 2025, an attacker phished npm maintainer qix and published malicious versions of 18 packages including chalk and debug.
  • The packages collectively have 2.6 billion weekly downloads. The malicious versions were live for roughly two hours.
  • The payload hooked browser-side wallet APIs to silently redirect crypto transactions. It failed due to a bug in the attacker's code.
  • 99% of cloud environments had at least one affected package. 10% pulled the malicious versions during the window.
  • The attack vector was social engineering, not a technical vulnerability. One phished maintainer was enough.
  • Protection: npm ci over npm install, committed lockfiles, and auditing your build pipeline timestamps.

FAQ

How do I know if my build pulled the malicious versions of chalk or debug?

The exposure window was roughly 13:15–15:30 UTC on September 8, 2025. If your CI/CD pipeline ran npm install during that window without a committed lockfile, check your resolved versions. The malicious versions were chalk@5.6.1 and debug@4.4.2, among others. Run npm list chalk debug in the relevant project to see what you resolved.

My app doesn't handle cryptocurrency. Was I still at risk?

The payload targeted browser-side crypto wallet APIs. If your app doesn't bundle these packages into client-side code, or doesn't interact with window.ethereum or Solana, the malicious payload had no practical effect on your users. The greater concern is server-side exposure — if the infected packages ran in an environment with access to secrets or credentials, treat those as potentially compromised.

Does using a lockfile fully protect against this kind of attack?

A committed lockfile used with npm ci would have protected you during this specific incident. If your lockfile was generated before the attack window and you installed from it exactly, you resolved the clean versions. But if your pipeline runs npm install without a lockfile, or regenerates the lockfile on each run, you were exposed to whatever the registry served during the build.

What's the difference between this attack and typosquatting?

Typosquatting creates a fake package with a name close to a legitimate one. This attack compromised the actual legitimate package by taking over the real maintainer's account. Developers installed exactly the packages they intended to — they just happened to contain malicious code at the moment of installation.

supply chainnpmchalkdebugdependenciessecurity incidentopen source