Key takeaways
- An npm supply chain attack delivers malicious code through a dependency, usually executed automatically by an install script or on import.
- The main attack types are typosquatting, dependency confusion, maintainer account takeover, malicious updates by insiders, protestware and self-propagating worms.
- In September 2025 a phished maintainer account was used to publish malicious versions of chalk, debug and other packages with billions of combined weekly downloads.
- The Shai-Hulud worm in 2025 stole npm tokens from developers and CI, then used them to publish infected versions of the victims' own packages.
- Effective defenses combine committed lockfiles, disabled install scripts, delayed adoption of new versions, phishing-resistant 2FA and source-level scanning.
When you run npm install you execute code written by hundreds of people you have never heard of, at versions you did not choose, with the same permissions as your shell. Supply chain attackers do not need to break into your systems. They need to get one of those hundreds of people, or the registry, to hand you their code. This guide walks through the attack types, the incidents that defined them, and the defenses that actually work.
Why npm is the biggest target
- Install-time execution. npm runs
preinstall,installandpostinstallscripts for every package in the tree by default. An attacker gets code execution on developer laptops and CI runners before anyone imports anything. - Deep trees. A single direct dependency can pull in hundreds of transitive ones. You approved one; you trust all of them.
- Semver ranges.
^1.2.3means "any 1.x release published in the future". A malicious 1.2.4 is picked up by every fresh install that is not pinned by a lockfile. - Valuable environments. Developer machines and CI hold npm tokens, cloud credentials, SSH keys, GitHub tokens and crypto wallets.
The main attack types
Typosquatting and brandjacking
The attacker publishes a package with a name close to a popular one and waits for typos, copy-paste errors or LLM hallucinations. A well-known early case was crossenv in 2017, a lookalike of cross-env that sent environment variables to the attacker. Variants include swapping separators (lodash.js, node-fetch-js), publishing unscoped copies of scoped packages, and "slopsquatting": registering names that AI coding assistants commonly invent. Typosquats are cheap and published in bulk; most are found within days, but a few downloads is all an attacker needs.
Dependency confusion
In February 2021 security researcher Alex Birsan showed that many companies' build tools would fetch a public package instead of an internal one with the same name, if the public version number was higher. He published harmless proof-of-concept packages matching internal names leaked in public JavaScript and manifests, and got code execution inside dozens of large companies. The fix is configuration: use a scope you own (@yourcompany/), map that scope to your private registry in .npmrc, and never let a resolver fall back to the public registry for internal names.
# .npmrc @yourcompany:registry=https://npm.internal.example.com/
Maintainer account takeover
The most damaging attacks hijack a real maintainer and publish a malicious version of a package people already trust. Nothing about the name or history looks wrong.
- ua-parser-js (October 2021). The maintainer's account was compromised and three malicious versions were published. They installed a cryptocurrency miner and, on Windows, a credential stealer. The package had millions of weekly downloads.
- coa and rc (November 2021). Both packages, dormant for years, received sudden new versions with a preinstall script that downloaded a password-stealing trojan on Windows. Because coa sat deep in common React toolchains, builds broke worldwide, which is partly how it was noticed so quickly.
- chalk, debug and others (September 2025). A prolific maintainer received a convincing phishing email from a lookalike npm support domain asking him to update his 2FA. The attacker captured his credentials and 2FA code and published malicious versions of around 18 packages, including chalk, debug, ansi-styles and strip-ansi, with billions of combined weekly downloads. The payload was a browser-side crypto drainer that hooked network and wallet APIs to swap recipient addresses. The versions were pulled within hours, but anyone who installed in that window, and bundled the code into a web app, shipped it to users.
Malicious takeover by a new maintainer
event-stream (2018) is the textbook case. The original author, no longer interested in maintaining it, gave publishing rights to a volunteer. The new maintainer added a dependency, flatmap-stream, whose published build contained an encrypted payload. It only decrypted when running inside the Copay bitcoin wallet's build, using that project's package description as the key, and then attempted to steal wallet funds. It went unnoticed for about two months. The lesson: a maintainer change is a security event, and the code you need to read is the published tarball, not the repository.
Protestware and sabotage
Here the maintainer is not compromised; they deliberately ship harmful behavior.
- colors and faker (January 2022). The author pushed versions that broke the packages: colors entered an infinite loop printing garbage text, and faker's code was removed. Thousands of projects that depended on them through ranges broke on their next install.
- node-ipc (March 2022). The maintainer added code that, for machines with IP addresses geolocated to Russia or Belarus, overwrote files with a heart emoji. It was assigned CVE-2022-23812. It reached many developers transitively, including through a popular Vue CLI dependency.
Protestware is a reminder that "trusted maintainer" is not a technical control. Code from a trusted source still needs review when it changes.
Self-propagating worms
In September 2025 the Shai-Hulud worm turned stolen credentials into automated spread. Infected packages ran a script that searched the host for secrets (using the open-source TruffleHog scanner along with environment variables and cloud metadata), exfiltrated them, including by pushing them to public GitHub repositories, and then used any npm token it found to publish trojanized versions of other packages that victim maintained. Hundreds of packages were affected in the first wave, and a larger second wave in November 2025 moved execution to a preinstall script. Worms change the economics: one compromised developer becomes many compromised packages within hours, without the attacker doing anything.
What the payloads actually do
Across all these incidents, payloads fall into a small set of behaviors:
| Behavior | Typical indicators |
|---|---|
| Credential theft | Reads process.env, ~/.npmrc, ~/.ssh, ~/.aws, browser profile databases |
| Exfiltration | HTTP POST to unknown hosts, DNS lookups with encoded subdomains, webhooks, pushes to GitHub |
| Second-stage loader | Downloads a binary or script and runs it via child_process |
| Obfuscation | Large base64/hex blobs, eval or new Function of decoded strings, string-array obfuscators |
| Wallet drainers | Hooks fetch, XMLHttpRequest or window.ethereum; replaces addresses |
| Miners and backdoors | Spawns long-running processes, reverse shells, writes to startup locations |
These are exactly the behaviors our scanner asks an LLM to look for in every file of every dependency. Flagged packages are listed on the malicious packages tracker.
Defenses that work
For teams consuming packages
- Commit the lockfile and use
npm ci. It installs exactly what the lockfile says and fails ifpackage.jsondisagrees, so a malicious new release cannot slip in through a range. - Disable install scripts by default.
npm config set ignore-scripts true, or use a package manager that requires an allow-list (pnpm, Bun). See install scripts security. - Wait before adopting new versions. Most malicious versions are detected and removed within hours to days. A cooldown of a few days on automated update PRs avoids most of them. Dependabot and Renovate both support delaying updates.
- Scope internal packages and pin the scope to your private registry to prevent dependency confusion.
- Scan for known advisories and for behavior.
npm auditor osv-scanner for reported issues, and source-level analysis for what has not been reported yet. See npm audit vs source scanning. - Limit blast radius. Do not keep long-lived publish tokens or production cloud credentials on developer machines or in every CI job. Run installs in containers with no secrets mounted.
For maintainers publishing packages
- Use phishing-resistant 2FA (a security key or passkey). The 2025 chalk/debug compromise defeated one-time codes by proxying them.
- Publish from CI using npm trusted publishing (OIDC) and provenance instead of long-lived tokens.
- Never follow links in emails about your account; go to npmjs.com directly.
- Treat any request to transfer ownership as a security decision.
Checking your project now
If you want to know whether your current dependencies contain anything like the payloads above, start with the manual steps in how to check if an npm package is malicious, or upload your lockfile to the scanner for a file-by-file review. Results are AI-generated guidance with false positives and negatives possible; the methodology explains what is and is not covered.
Frequently asked questions
What is an npm supply chain attack?
It is an attack where malicious code reaches your application or machine through an npm dependency rather than your own code. It typically runs automatically through an install script or when the package is imported.
What was the event-stream attack?
In 2018 a new maintainer of the popular event-stream package added a dependency, flatmap-stream, containing an encrypted payload that targeted the Copay bitcoin wallet. It only activated inside Copay's build and tried to steal wallet funds.
What was Shai-Hulud?
Shai-Hulud was a self-propagating npm worm first seen in September 2025. It stole secrets and npm tokens from infected machines and used those tokens to publish malicious versions of the victims' other packages.
How does dependency confusion work?
A build tool configured to check both a private and the public registry may pick a public package with the same name as an internal one if its version is higher. Using an owned scope mapped to your private registry prevents it.
Does a lockfile protect me from supply chain attacks?
It protects you from malicious new versions slipping in through semver ranges, as long as you install with npm ci. It does not protect you if the locked version is already malicious, so the locked tree still needs scanning.