The Invoice ■ Episode 03
"But it's just one dependency!"
That sentence has done more damage to software security than any zero-day exploit. It sounds reasonable. It feels proportionate. And it is the argument that installs 1,400 packages before your kettle has boiled.
The Arithmetic of Trust
One package installs twenty. Each of those installs ten more. The growth is not linear; it is exponential, and the numbers are not hypothetical. Express.js: one framework, fifty-seven dependencies. create-react-app: you approved one package and received north of 1,400.
Now ask yourself: did you vet Express? Perhaps. Did you vet its fifty-seven dependencies? Their dependencies? The maintainer three levels deep -- the one whose name you have never encountered, whose code quietly processes your user input -- did anyone audit that?
The honest answer, in virtually every project on Earth, is no.
Three Principles, Systematically Violated
Information security has operated on a small set of foundational principles for decades. They are not controversial. They are not new. And npm violates all three, by design, every single day.
Never execute untrusted code. npm install runs arbitrary scripts from strangers. Verify before trust. The dependency tree is too deep and too wide for meaningful verification. Minimise attack surface. Each additional package is another door you cannot lock, another window you cannot watch.
These are not edge cases. This is the default mode of operation for the largest package registry on the planet.
The Casualty List
If this were merely theoretical, one might file it under "known risk, accepted trade-off" and move on. It is not theoretical.
In 2016, a developer unpublished
a package called left-pad -- eleven lines of code that padded a string with spaces. The cascading
failure took down build systems across the industry. Thousands of projects broke because
they had delegated string padding to a stranger. The function, incidentally, is now
built into JavaScript itself via
String.prototype.padStart(),
shipped natively since ES2017.
In 2018, event-stream, a package with eight million weekly downloads, was handed to a new maintainer who injected code designed to steal cryptocurrency. The attack was elegant, targeted, and went undetected for weeks.
In 2021, ua-parser-js, seven million weekly downloads, was compromised to distribute cryptominers. The package parses user-agent strings. The attack payload mined currency.
Three incidents. Three different years. The same structural defect.
The Security Theatre
Your security team audits your code. Commendable. But who audits the 1,400 packages you have silently delegated trust to?
Dependabot? Snyk? They scan for known vulnerabilities -- that is, vulnerabilities discovered and catalogued after someone has already been harmed. They are plasters applied to a structural wound. Snyk raised $530 million on that business model, which tells you rather more about the size of the wound than the quality of the plaster.
npm hosts 2.1 million packages -- the largest code pile on Earth. According to Sonatype's State of the Software Supply Chain report, over half are effectively unmaintained. You are running production systems on code whose authors have moved on, lost interest, or never intended it for production in the first place.
The Root Cause
How did we arrive here? The answer is disarmingly simple: JavaScript shipped without a standard library.
No HTTP client. No cryptography. No date formatting that actually works. Rather than fixing the language -- rather than shipping batteries, as Python does, or building rich standard libraries, as Go and Rust have done -- the ecosystem reached for npm. npm became the de facto standard library. 2.1 million packages later, we are living with the consequences.
The reductio ad absurdum is a package called is-odd, which at its peak enjoyed 400,000 weekly downloads. Its purpose: to determine whether a number is odd. The implementation is one line of arithmetic:
return n % 2 === 1;
Four hundred thousand developers per week chose to install, download, and trust a third-party package rather than write a single expression. This is not laziness. This is a culture that has normalised outsourcing judgement to strangers.
The Compound Interest
The npm tax is not a one-off payment. It compounds. Every dependency you add is a subscription to someone else's maintenance schedule, security posture, and architectural opinions. When they change, you change. When they break, you break. When they vanish, you scramble.
The alternatives exist and have existed for some time. Write fewer dependencies and more
of your own code -- the function is one line, after all. Evaluate whether Deno or Bun,
with their more considered standard libraries, suit your constraints.
Use padStart(), which has been native since 2017, instead of installing a
package to pad strings.
Or continue as before. The node_modules folder that weighs more than your application is not a feature. It is an invoice. And like all invoices, it comes due.