Back to Blog
Security11 min

Your Dependencies Have Dependencies, and They're Terrible

Transitive dependencies are the real attack surface. Most teams audit direct deps and ignore the 95% hiding underneath.

By Security TeamMarch 4, 2026

A React project with 12 direct dependencies pulled in 1,847 packages last week. The team knew about those 12. They had no idea about the other 1,835. One of them, four levels deep in the tree, hadn't been updated since 2019 and had a prototype pollution vulnerability that let attackers inject arbitrary properties into every object in the runtime.

Nobody noticed for seven months.

What Actually Lives in node_modules

Run npm ls --all | wc -l on any non-trivial project. The number will make you uncomfortable. A fresh Next.js app ships with around 300 packages. Add a handful of UI libraries, an ORM, maybe a testing framework, and you're easily past 1,000. Each of those packages made its own choices about what to depend on, and those choices compound.

The math is brutal. Say you directly depend on 20 packages. Each of those averages 15 transitive deps. That's 300 packages you never chose, never reviewed, and probably never heard of. Some of them are maintained by a single person who pushes code from a laptop with no 2FA. Some haven't seen a commit in three years. And your production build trusts every single one of them.

Why npm audit Gives You False Confidence

npm audit checks the National Vulnerability Database. That's it. If a vulnerability hasn't been reported, assigned a CVE, and added to the advisory database, audit won't flag it. The lag between a vulnerability being exploitable and getting a CVE can be weeks or months. The event-stream incident in 2018 was actively stealing Bitcoin wallet keys for weeks before anyone caught it, and npm audit said everything was fine the entire time.

Then there's the noise problem. Run audit on a medium project and you'll get 47 warnings, 30 of which are in dev dependencies that never touch production, and 12 that require breaking changes three major versions deep to fix. Teams stop reading the output. It becomes the boy who cried wolf, except sometimes the wolf is actually there.

$ npm audit
found 47 vulnerabilities (12 moderate, 31 high, 4 critical)

# The team's response, every single time:
$ npm audit fix
# fixes 3 of 47
# the other 44 require --force which breaks everything

Lockfile Drift and the Monday Morning Surprise

Lockfiles exist to prevent exactly this problem. Pin your versions, get reproducible builds, sleep at night. Except teams mess this up constantly.

Someone runs npm install new-package locally, which regenerates parts of the lockfile. They commit it without reviewing the diff because who actually reads a 2,000-line lockfile change? Meanwhile, a transitive dependency got bumped from 2.3.1 to 2.4.0, and that minor version introduced a new dependency on a package maintained by someone who just sold their npm account.

Worse: some teams don't even commit lockfiles. Or they have a .gitignore that excludes package-lock.json because "it causes merge conflicts." That's like removing your seatbelt because it wrinkles your shirt.

The Phantom Dependency Problem

Node's module resolution algorithm lets you import packages you never declared as dependencies. Your code does require('some-util'), it works because some-util happens to be a transitive dep of something you actually installed, and nobody notices until you upgrade the parent package and some-util disappears. Suddenly CI breaks and nobody knows why because the import has been there for two years.

// This works fine... until it doesn't
const flatMap = require('flatmap-stream');
// You never installed this. It's a transitive dep of event-stream.
// When event-stream@4.0.0 drops it, your build explodes on a Friday at 5pm.

pnpm fixes this with strict isolation. Yarn PnP does too, in its own way. But most teams are still on npm with the default hoisting behavior, living on borrowed time.

Attacks That Target the Middle of the Tree

Attackers figured out that nobody watches transitive dependencies. The playbook is straightforward:

  1. Find a small, widely-used utility package deep in common dependency trees
  2. Offer to maintain it (the solo maintainer is burned out and will gladly hand over access)
  3. Wait a few months, push some legitimate fixes to build trust
  4. Slip malicious code into a patch release
  5. Wait for automated CI/CD pipelines to pull the update

This is exactly what happened with ua-parser-js in October 2021. 7 million weekly downloads. The attacker compromised the maintainer's npm account and published versions that installed cryptominers and credential stealers. Because it was a patch bump, every project with a ^ range pulled it automatically. The window was only a few hours, but that was enough.

The colors and faker incident was different but equally instructive. The maintainer themselves pushed destructive updates to protest open source economics. Not a hijacked account. Not a supply chain attack in the traditional sense. Just a frustrated developer who decided 8 billion free downloads was enough. npm audit doesn't have a check for "maintainer went rogue."

What Actually Works

Pin Everything, Review Everything

Use exact versions in package.json. Yes, you'll miss automatic patch updates. That's the point. Every dependency change should be deliberate, reviewed, and traceable. Tools like Renovate or Dependabot can automate the PRs while keeping humans in the approval loop.

// Bad: lets transitive deps float
"dependencies": {
  "express": "^4.18.0"
}

// Better: exact version, upgrade intentionally
"dependencies": {
  "express": "4.18.2"
}

Actually Read the Lockfile Diffs

Add a CI check that flags lockfile changes larger than a threshold. If package-lock.json changed by more than 100 lines, require explicit approval. lockfile-lint can verify that packages come from expected registries and haven't been tampered with.

Use Socket.dev or Similar Tools

Socket analyzes package behavior, not just known CVEs. It flags packages that suddenly start accessing the network, reading environment variables, or running install scripts. This catches the attacks that advisory databases miss entirely.

Shrink Your Attack Surface

Every dependency you remove is a dependency you don't have to secure. Before adding a package, check: does it do something you could write in 50 lines? Is the left-pad situation really necessary? The is-odd package on npm has 500,000 weekly downloads. It checks if a number is odd. That's a one-liner. But now you've added it to your supply chain.

// 500,000 weekly downloads for this
function isOdd(n) { return n % 2 !== 0; }

// Just... write it yourself. Please.

Scanning Beyond the Surface Level

Most dependency scanners stop at CVE databases. That catches maybe 30% of the actual risk. The rest is unmaintained packages, phantom dependencies, unnecessary permissions, and packages whose maintainers could disappear tomorrow. ScanMyCode.dev runs a full dependency audit that goes beyond npm audit — checking maintenance status, license compliance, known vulnerabilities, and transitive risk. You get a report showing exactly which packages in your tree are problems and what to do about them.

Stop Treating Dependencies as Someone Else's Problem

The uncomfortable reality is that every package in your lockfile is your responsibility. You chose the framework that chose the library that chose the utility that got compromised. "But it was a transitive dep" doesn't matter to your users when their data is gone.

Start with visibility. Run npm ls --all and actually look at what you're shipping. Check the maintenance status of your top-level deps. Set up automated lockfile monitoring. And stop ignoring npm audit output just because most of it is noise — filter it to production deps only with npm audit --omit=dev and deal with what's left.

Your direct dependencies are the front door. Your transitive dependencies are 200 unlocked windows you forgot existed. Run a dependency audit and find out what's actually in your supply chain before someone else does.

dependenciessupply-chaintransitive-dependenciessecuritynpmlockfile

Ready to improve your code?

Get an AI-powered code audit with actionable recommendations. Results in 24 hours.

Start Your Audit