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.
Try this. Run these two commands in a fresh folder:
bashnpx create-next-app@latest test-app npm ls
Do it and actually read the output. My last run installed a handful of direct dependencies — and then npm ls listed 800+ transitive packages underneath them. Eight hundred small programs, written by strangers, maintained by volunteers, running inside my application with the same file system, network access, and environment variables I have.
That's not a dependency tree. That's an attack surface. And it's the part of modern software development I find hardest to defend, because almost nobody actually looks at it.
How these attacks actually happen
The three attacks that matter:
Typosquatting. Someone publishes expresss with three s's, or a package named like a popular library plus a hyphen suffix. A developer fat-fingers an install command, and suddenly they're executing arbitrary code from a stranger's account. npm's security team removes malicious packages by the thousands every year, and typosquats are a steady share of them — but takedowns happen after the damage, not before.
Dependency confusion. Your company uses an internal package called @company/utils that never gets published publicly. An attacker publishes the same name to the public npm registry. Depending on how your package manager resolves versions, the next build might pull the public, malicious copy instead of your private one. This is a real, demonstrated technique — researchers have used it against major companies' internal infrastructure.
Maintainer compromise. The scary one, because nothing you did was wrong. A widely used package's maintainer gets their account hijacked, or burns out and hands the project to someone malicious, or a new contributor gets merge access and sneaks in a backdoor. The event-stream incident in 2018 is the textbook case: a popular library with millions of weekly downloads was quietly backdoored by a new maintainer who was specifically targeting cryptocurrency wallets. It shipped to everyone who ran npm install during that window.
Why it's so hard to defend
I used to think the answer was just "be careful," but the deck is genuinely stacked:
- Updates are automatic by default. Those
^and~ranges in yourpackage.jsonmean a "patch" release can install anything. Nobody reads the diff of every transitive update. - You don't choose your sub-dependencies. Your dependencies do. You have effectively zero control over what sits three or four levels deep.
npm installexecutes code. Postinstall scripts run arbitrary commands when a package is installed. Most people think installing is "downloading files." It isn't.- Audit fatigue is real. Run
npm auditon any real project and the warning list is so long that teams learn to ignore it or suppress it. The tool exists; the habit of acting on it mostly doesn't.
What actually moves the needle
I can't make the ecosystem trustworthy, so I work on shrinking how much of it I trust:
- Pin versions and commit the lockfile. No floating ranges for things that matter. If it upgrades, it upgrades deliberately, in a commit I can see.
- Question every new dependency. Before adding a package, I ask if I can write what I need in fifty lines. More often than you'd think, the answer is yes, and fifty lines of my own code is a smaller attack surface than someone else's 4,000.
- Disable install scripts where I don't need them.
npm install --ignore-scriptsexists for a reason. If a package needs a build step to work, I want to know about it — not have it run silently. - Read the changelog and diff on major upgrades. Ten minutes of scanning before bumping a core dependency has caught real problems for me.
None of this is exotic. It's just not doing the one thing everyone does by default: trusting the tree without ever looking at it.
The uncomfortable truth is that the biggest risk in your application probably isn't an elite hacker finding a zero-day in code you wrote. It's the package you installed six months ago that you've never opened, maintained by someone you've never heard of, with full access to everything your app can touch. That's worth remembering the next time your fingers type npm install <thing> on autopilot.
Enjoyed this read?
Share it with your network.