When you install a library from npm, you don't install a library. You install a library, plus all of its dependencies, plus the dependencies of those dependencies, plus theirs.
A React project with a reasonable dependency list can easily end up with 500 or 600 packages in node_modules. You wrote the code for maybe five of them.
The other 595 were written by people you've never met, maintained in spare time those people sometimes don't have, and published to a registry that lets anyone with an account upload whatever they want.
That's an attack vector. It's called a supply chain attack.
What a supply chain attack is
A supply chain attack doesn't target your code. It targets the code your code trusts.
Instead of trying to compromise your application directly — which means learning it, finding a vulnerability, and exploiting it — the attacker steps back one level and compromises a dependency your application installs automatically.
The malicious code reaches production not because someone hacked your system, but because you ran npm install the way you always do.
Why npm is particularly vulnerable
Three traits of the npm ecosystem create favorable conditions for this kind of attack:
The open publishing model. Anyone can create an account and publish a package. There is no prior review. The same openness that grew the ecosystem is what makes it hard to control.
The depth of the dependency tree. A package you install directly can depend on ten more, each of which depends on others. An attacker doesn't need to compromise a library you use directly — being buried somewhere in the tree is enough.
Install scripts. npm lets packages run scripts automatically during installation (postinstall, preinstall). Code that runs on your machine, with your permissions, before you've finished reading what you installed.
Three common attack vectors
Typosquatting. Publishing packages with names nearly identical to popular ones. lodash vs 1odash. express vs expres. One typo in an npm install and you're executing malicious code.
Maintainer account compromise. Unauthorized access to a legitimate developer's account to inject code into a package that already has millions of downloads. Nobody has to be tricked into installing a suspicious package — the legitimate package is already installed everywhere.
Dependency hijacking. Slipping a malicious package into a fourth- or fifth-level dependency, where nobody looks and auditing tools rarely reach.
The real cases that changed how we look at dependencies
Event-Stream (2018) is the case that shifted the conversation most. A developer handed ownership of the package to a stranger who offered to help maintain it. The new maintainer injected code that specifically targeted Bitcoin wallets. The package had millions of weekly downloads. The attack reached production in projects all over the world before anyone caught it.
eslint-scope (2019) compromised the maintainer's account and injected a script that tried to steal environment variables — tokens, credentials, API keys — from wherever it ran. The npm team caught it quickly, but the exposure window was real.
coa (2022) showed you don't need to use a package directly to be exposed. coa is a deep dependency of many webpack projects and build tools. The compromise hit anyone who had coa at any level of their dependency tree — without knowing it.
The lesson from all three is the same: your dependencies are only as secure as the packages they trust.
How to protect yourself
Supply chain security isn't a problem you solve once. It's an ongoing practice.
Audit often. npm audit is the entry point, but tools like Snyk or Dependabot give more context and prioritize better. Wire this into your CI pipeline — if a dependency has a known vulnerability, the build should know before it reaches production.
Use lockfiles and treat them as code. package-lock.json or yarn.lock pin the exact version of every transitive dependency. If you don't commit the lockfile, every npm install can pull different versions. Running npm ci instead of npm install in CI guarantees the lockfile versions are the ones installed.
Look at what you're about to install before you install it. For new packages, check the repository: is there recent activity? how many maintainers does it have? does the download count make sense for what it does? A package with 50 stars and 2 million weekly downloads is a strange signal.
Minimize postinstall scripts. You can see what scripts a package runs before installing it. For critical projects, consider npm install --ignore-scripts and audit which scripts you actually need to enable.
Never put secrets in the code. If an attacker gets code execution in your build environment, what they can steal depends on what's available. Environment variables injected by CI are harder to exfiltrate than a committed .env or credentials sitting in config files.
Make scanning part of the pipeline. Not as an optional step — as a gate. A pipeline that fails when dependencies have known vulnerabilities is what makes the team take them seriously.
Train the team. Most supply chain incidents have a moment where someone could have asked a question. A team that knows what signals to look for — a package that suddenly changes maintainers, an update that adds new install scripts, a dependency that shows up in the lockfile without anyone installing it directly — is harder to fool.
What you can't eliminate, you can reduce
No strategy makes the npm ecosystem completely safe. The same open model that made it so productive creates an attack surface that doesn't go away.
What you can do is reduce the probability of exposure and the severity of the impact when it happens. Lockfiles, automated audits, least privilege in build environments, and a team that understands the risk are, together, a far stronger defense than ignoring the problem.
You're going to run the next npm install anyway. The question is whether you know what comes with it.