Your Dependency Tree Is an Attack Surface
We blindly trust packages from strangers on the internet. Here's why that's terrifying and what to do about it.
I recently ran npm ls on a fresh Next.js project. It had 12 direct dependencies and over 800 transitive ones. Eight hundred packages — written by strangers, maintained by volunteers, running inside my application with full access to everything.
That's not a dependency tree. That's an attack surface.
The Hidden Scale
When you run npm install, you're trusting:
- The package author's intentions
- Their operational security
- Every maintainer of every sub-dependency
- The npm registry's integrity
- The build pipeline of each upstream package
A typical web app might list 15-20 packages in package.json. But the actual number of packages being executed is often 500-1000+. Most developers have no idea what 95% of those packages actually do.
How Attacks Work
Typosquatting
Someone publishes expresss (three s's) or react-dom-utils (not a real package from the React team). Developers install it by mistake, and now they're running arbitrary code. This happens regularly — npm catches hundreds of typosquat packages every month.
Dependency Confusion
Your company uses a private internal package called @company/utils. An attacker publishes a public package with the same name on npm. Depending on your package manager's resolution strategy, it might install the public (malicious) one instead.
This attack was famously demonstrated against Apple, Microsoft, and Tesla. It works because package managers trust the public registry by default.
Maintainer Compromise
A trusted, widely-used package gets compromised because:
- The maintainer's npm account gets hijacked
- The maintainer gets burned out and hands the project to someone who turns out to be malicious
- A new contributor gets merge access and pushes a backdoor
The event-stream incident in 2018 is the textbook example. A popular library with millions of weekly downloads was backdoored by a new maintainer who specifically targeted cryptocurrency wallets.
Why It's Hard to Defend
- Auto-updates — If you use
^or~version ranges (the default), new versions install automatically. A patch release can contain anything. - Transitive depth — You don't pick your sub-dependencies. Your dependencies do. You have zero control over packages three or four levels deep.
- Audit fatigue — Running
npm auditon a real project produces so many warnings that most teams either ignore them or suppress them. - Build-time execution —
postinstallscripts run arbitrary code when you install a package. Most developers don't realizenpm installis executing code, not just downloading files.
What Actually Helps
- Pin your dependencies. Use exact versions and a lockfile. Don't let packages auto-update.
- Minimize your tree. Before adding a package, ask: can I write this in 50 lines myself? If the answer is yes, skip the dependency.
- Disable postinstall scripts for packages you don't trust.
--ignore-scriptsexists for a reason. - Read the diff before updating major dependencies. At minimum, scan the changelog.
- Use tools like Socket.dev, Snyk, or npm's built-in
npm audit— but don't blindly trust them either.
What Keeps Me Up
Every dependency you install is an external actor inside your system. Same permissions as your code. Same access to your file system, environment variables, network. There's no sandbox. There's no permission model. It's all-or-nothing trust.
The biggest risk in your application isn't some elite hacker finding a zero-day in your code. It's the package you installed six months ago that you've never looked at, maintained by someone you've never heard of, with full access to everything your app can touch.
That's worth thinking about the next time you run npm install.
Enjoyed this read?
Share it with your network.