npm install && npm pray#
Run npm install on a fresh project. Watch the counter tick. 847 packages. You chose maybe 12 of those — Express, a test framework, a handful of utilities you actually evaluated. The other 835? Transitive dependencies. Packages you’ve never heard of, maintained by people you’ve never met, running code you’ve never read.
Every one of them has full access to your filesystem at install time. Every one of them could execute arbitrary scripts in postinstall. And you just trusted all 835 of them because a package you did choose listed them in its dependency tree.
This is software supply chain security, and it’s scarier than you think.
The Hall of Shame#
A few greatest hits, in case you missed them.
left-pad (2016). An 11-line npm package that left-pads strings. The author unpublished it after a naming dispute. Babel, React, Netflix, and PayPal broke instantly. The entire JavaScript ecosystem went down because of eleven lines that did what String.prototype.padStart does today. npm responded by making it impossible to unpublish packages that other packages depend on. The fix was policy, not technology.
event-stream (2018). A popular streaming utility with millions of weekly downloads. The original maintainer was burned out, so when a helpful stranger offered to take over maintenance, he handed over publishing rights. The new maintainer added a dependency called flatmap-stream that contained an encrypted payload targeting a specific Bitcoin wallet app. It sat there for two months before anyone noticed. Over 800,000 projects were affected.
colors.js (2022). The maintainer of colors and faker — packages with combined millions of weekly downloads — pushed an update that added an infinite loop printing “LIBERTY LIBERTY LIBERTY” to the console. It wasn’t a hack. The maintainer did it on purpose, protesting that Fortune 500 companies were using his work without compensating him. Thousands of projects broke, including the AWS CDK.
The pattern is always the same: a package everyone depends on, maintained by one person, with zero oversight. The blast radius is absurd because the trust model is “well, it’s on npm.”
Name Your Poison#
The incidents above made headlines. The attacks happening right now are quieter and more creative.
Dependency confusion is my favorite (in a “this is terrifying” way). Alex Birsan discovered that when companies use private package registries alongside public ones, the package manager often prefers the public registry if it finds a matching name with a higher version number. He registered public packages with the same names as internal packages at Apple, Microsoft, and Uber, set the version to 999.0.0, and collected $130,000 in bug bounties as their CI/CD pipelines auto-installed his code.
The attack is elegant because it exploits how package managers resolve dependencies by design. No credentials stolen, no social engineering — just a version number.
Typosquatting is the blunter version. Register crossenv instead of cross-env, or lodahs instead of lodash, and wait for someone to fat-finger their install command. In 2024 alone, researchers found over 3,000 malicious typosquat packages on npm. Some included cryptominers. Some exfiltrated environment variables (which is where your API keys live, if you were wondering).
The Lockfile Lies (By Omission)#
“But I have a lockfile” is the most common response when I bring this up. And yes — lockfiles are essential. They pin exact versions, store integrity checksums, and freeze your entire dependency graph so that everyone on the team gets identical node_modules.
What they don’t do:
They don’t prevent updates when you don’t want them. npm install can silently update your lockfile. If someone adds a new package or runs install on a machine with a different npm version, the lockfile changes. The lockfile is only as frozen as your discipline. This is why npm ci exists — it reads the lockfile as-is and fails if there’s a mismatch, instead of quietly “fixing” it. Use it in CI. Always.
They don’t audit what’s inside the packages. Your lockfile knows that unpipe@1.0.0 has SHA abc123. It doesn’t know what unpipe@1.0.0 does. You audited Express. You did not audit unpipe, raw-body, on-finished, ee-first, or the other 40-something transitive dependencies Express pulls in. And unpipe has one maintainer. When’s the last time anyone looked at that code?
They don’t protect against registry compromise. If a maintainer’s credentials get stolen (see: ua-parser-js in 2021 — 7 million weekly downloads, compromised via account takeover, shipped cryptominers), the lockfile happily pins the malicious version the next time you install.
The lockfile is a seatbelt. It is not a rollcage.
The Iceberg Below the Waterline#
Here’s the mental model that changed how I think about this: the transitive dependency iceberg.
Your package.json (what you chose):
├── express
├── jest
└── dotenv
express alone pulls in:
├── body-parser
│ ├── raw-body
│ │ ├── unpipe ← 1 maintainer, last published 2015
│ │ └── iconv-lite
│ ├── on-finished
│ │ └── ee-first ← 1 maintainer, last published 2014
│ └── qs
├── cookie
├── ... (~30 more)
You’re responsible for the security of your application. Your application includes all of these. When’s the last time you ran npm audit? And when it flagged 17 moderate vulnerabilities in packages four levels deep, what did you actually do about it?
(If the answer is “I ignored it because there was no patch available” — same. That’s the honest answer. But it’s worth being uncomfortable about.)
Fortifying the Castle#
Here’s the practical part. None of this requires paranoia — just awareness and a few changes to your workflow.
Use npm ci (or bundle install --frozen) in CI. Never let your pipeline resolve dependencies dynamically. The lockfile should be the source of truth, and the build should fail if reality doesn’t match. This is table stakes.
Run npm audit regularly and triage the results. Not every vulnerability is exploitable in your context. A prototype pollution in a dev dependency isn’t the same as an RCE in a production dependency. But you should know about them.
Pin your dependency updates. Use Renovate or Dependabot to get automated PRs when dependencies update. Renovate gives you more control — you can group updates, auto-merge patch versions, and set schedules. The point isn’t to update everything immediately; it’s to know when things change and make deliberate decisions.
Generate an SBOM. A Software Bill of Materials (CycloneDX or SPDX format) is a machine-readable list of every dependency in your application. If a new CVE drops for a package you’ve never heard of, an SBOM lets you grep for it in seconds instead of doing archaeology through your lockfile. OWASP Dependency-Track can monitor your SBOMs continuously.
Reduce your surface area. Before adding a dependency, ask: do I actually need this? is-odd is a real npm package. It has one dependency — is-number. is-number is 293 lines of code
for what could be typeof x === 'number'. Not every utility needs to be imported. Sometimes the best dependency is the one you don’t add.
836 Strangers#
Supply chain security made the OWASP Top 10 in 2025 (A03, if you’re keeping score). That’s not because the problem is new — it’s because the industry finally admitted it’s a real threat vector and not just a niche concern for security researchers.
Your lockfile has 847 packages. You chose 12. The other 835 are strangers you’re running in production with full trust.
You don’t need to audit every one of them. But you should at least know they’re there.
