Incident Analysis

debug/chalk npm Compromise Sept 2025: Deep Dive

A phishing campaign against a prolific npm maintainer poisoned chalk, debug, and several other packages with a Web3 hijacker. Here is the full breakdown.

Shadab Khan
Security Engineer
7 min read

In September 2025, a phishing attack against Josh Junon ("qix"), a prolific npm maintainer, resulted in at least 18 packages being published with malicious code. The affected packages include chalk, debug, ansi-styles, strip-ansi, color-convert, color-name, and several others. Combined, they account for around 2 billion weekly downloads on npm. These are some of the most deeply embedded utility packages in the JavaScript ecosystem; nearly every Node.js project depends on them transitively.

The incident is instructive because despite enormous theoretical blast radius, actual reported losses were low. Understanding why tells us something about which controls actually worked.

How did a single phishing attack compromise 18+ packages?

The attacker emailed the maintainer from a spoofed support@npmjs.help domain, pushed a 2FA reset, and captured session credentials used to publish updates.

Junon has publicly documented the phishing flow. The email claimed to be an npm support notification requiring 2FA re-verification. He followed the link, entered credentials, and completed what appeared to be a normal 2FA flow on an attacker-controlled proxy. The proxy relayed the real 2FA to npm and captured the authenticated session. With session access, the attacker published new versions of every package the maintainer owned, within roughly two hours.

The key structural detail: npm sessions are persistent and can publish without re-prompting for 2FA under default settings. Once the session was captured, the attacker had publish rights across a huge surface area with no additional friction.

What did the payload do?

The injected code ran in browser contexts, intercepted Web3 wallet interactions, and swapped recipient addresses on-the-fly.

The payload was a browser-side hijacker. On execution, it hooked into window.ethereum and similar Web3 provider interfaces and monitored calls for transactions. When a matching transaction was observed, it swapped the destination address in the transaction payload to an attacker-controlled address, using visual-similarity techniques (addresses with matching prefixes and suffixes) to evade casual review by the user. The result: a user sending funds to a legitimate address signs a transaction that sends to the attacker instead.

Critically, this was a browser-only payload. It did not fire in Node.js server contexts. That is relevant because chalk and debug are primarily server-side utilities; they typically do not run in browsers. The payload was designed to activate only in the narrow slice of cases where a build bundler pulls the poisoned version into a frontend bundle that is then served to users.

Why were reported losses low despite the scale?

Three factors limited actual exploitation: fast takedown, the browser-only payload, and widespread use of lockfiles in bundler pipelines.

The malicious versions were unpublished within approximately two hours of initial publication, which is fast for an npm incident. Most CI systems running npm install within that window pulled poisoned versions, but most production deployments rolled forward on a slower cadence and never picked them up before the yank. Frontend bundler pipelines typically use package-lock.json with integrity hashes, which refused the poisoned versions because the hashes did not match.

The second factor: the payload targeted Web3 transactions in browsers. Most sites that accidentally bundled the poisoned chalk or debug are not Web3 sites, so the payload never found a wallet to hijack. Public reports pinned actual stolen funds in the low tens of thousands of dollars, which is a tiny fraction of the theoretical exposure.

The third factor: heavy community monitoring. Within minutes of the first suspicious publish, diffing tools flagged the bundle delta, and coordinated takedown started immediately.

What does this reveal about the npm phishing threat?

A targeted phishing attack can take a single maintainer and poison packages with billions of weekly downloads, and current defenses are not sufficient.

Josh Junon is a security-aware maintainer. He had 2FA. He followed standard best practices. The phishing attack still succeeded because session-based authentication flows are, by design, unable to distinguish between a legitimate user session and a stolen-but-valid session. Hardware-backed WebAuthn that signs a challenge including the origin domain would have failed closed because the attacker's proxy domain was not npm.com; the hardware key would have refused. But WebAuthn is not widely adopted for npm publishing.

There is an uncomfortable conclusion: the ecosystem's defense against this class of attack is a small number of diligent maintainers, and they are one convincing email away from a multi-billion-download compromise. Structural defenses must come from the registry side (mandatory phishing-resistant auth for publishers of widely-used packages) and the consumer side (reproducible builds, provenance verification, lockfile discipline).

What should consumer teams actually do?

Five actions, from immediate to structural.

Audit every recent install that occurred during the exposure window. Search package-lock.json files committed between the publish times and the yank times for any entry matching the poisoned version numbers. Rebuild affected builds from clean versions.

Enforce npm ci (not npm install) in CI, with committed lockfiles and integrity hashes. This is the single most effective defense against published-malware attacks and it would have prevented most of the theoretical exposure in this incident.

Set a default ignore-scripts=true in your npm config, and audit which packages you allow to run install scripts. For this specific incident the payload ran at runtime rather than install-time, but the pattern is broadly valuable.

Adopt npm provenance where possible. Packages published with OIDC-based trusted publishing include a signed attestation linking the package to a specific CI workflow and commit. Consumer-side tools can verify these attestations and refuse packages without them.

Invest in behavioral monitoring of your dependency graph. Automated tools that compare published package versions and alert on new network egress, new code paths touching crypto APIs, or unusual size deltas would have flagged this incident within minutes of publication. This is the new minimum for any team with nontrivial downstream exposure.

What should npm do structurally?

The registry itself is the right place to fix several of these problems, and some changes are already underway.

npm has begun rolling out phishing-resistant 2FA requirements for publishers of popular packages. Provenance-based publishing through OIDC is available and encouraged. Malware scanning pipelines ingest new publishes and flag suspicious patterns. But the default configuration for most accounts still allows session-based publishes with TOTP 2FA, which is exactly what the qix phishing attack exploited. Making WebAuthn mandatory for publishers of any package above a download threshold would eliminate the specific attack vector used in this incident. The resistance is user experience: WebAuthn requires a physical security key or platform authenticator, and some publishers find the onboarding cost too high.

A second structural change would be mandatory cooling-off periods for newly published versions of high-download packages. If a new version of chalk could not be installed by anyone for a configurable delay after publication (say, 30 minutes), the detection window widens substantially. Security researchers and automated scanners would have a fair chance to flag issues before propagation. Some ecosystems (Go's checksum database, for instance) already have mechanisms that create this kind of delay effectively. npm does not.

A third change would be richer provenance metadata on every package. Not just "this was published from this workflow" but "this was built from these exact sources with this exact toolchain, and the build is reproducible." That level of provenance is expensive to produce and expensive to verify, but it is the right long-term direction for a registry that underpins critical infrastructure.

These are not changes a consumer team can make, but they are worth articulating because the right long-term response is a combination of consumer-side discipline and registry-side structural improvements. Expecting consumers to compensate forever for registry defaults is not sustainable.

How Safeguard.sh Helps

Safeguard.sh's reachability analysis evaluates whether the injected Web3-hijacker code paths are actually reachable in your frontend bundles, applying the 60-80% noise reduction to distinguish real exposure from theoretical exposure on a graph this broad. Griffin AI builds behavioral baselines for every package version and would have flagged the new browser Web3 API interactions in chalk and debug within minutes of publication, independent of the npm registry's own signals. The SBOM pipeline tracks deeply transitive dependencies at 100-level depth, so teams can query "which of our builds bundle the compromised versions" immediately. TPRM gates widely-embedded utility packages under strict review, and container self-healing rolls back affected images as soon as upstream compromise is confirmed.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.