Why Dependency Security Is a Bigger Problem
A look at how modern software supply chains quietly expand attack surfaces beyond developer control.
Why Dependency Security Is a Bigger Problem Than Most Hackers
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.
The Uncomfortable Reality
Every dependency you install is an external actor inside your system. It runs with the same permissions as your code. It has access to your file system, your environment variables, your 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 should make you uncomfortable. It makes me uncomfortable. And that discomfort is a good starting point for taking supply chain security seriously.
Found this useful?
Share it with your network.