Skip to main content
the invisible-layer how abstraction is making software engineers dumber

The Supply Chain You Never Audited

9 min read Chapter 37 of 56
Summary

Explores how modern dependency ecosystems create massive unaudited...

Explores how modern dependency ecosystems create massive unaudited trust surfaces, examines real supply chain attacks like event-stream, ua-parser-js, and colors.js, and provides practical strategies for dependency auditing and risk mitigation.

The Supply Chain You Never Audited

Run this command in any non-trivial Node.js project:

npm ls --all 2>/dev/null | wc -l

A fresh Next.js application returns a number north of 800. A mature production application routinely exceeds 1,500. A React application with a component library, state management, testing, linting, and build tooling can cross 2,000 without anyone making extravagant choices.

Each line represents a package. Each package contains code that executes in your build pipeline or your production runtime. Each package was written by someone you’ve never met, maintained under processes you’ve never reviewed, hosted on infrastructure you don’t control.

How many of those 1,500 packages have you read? Not skimmed the README or glanced at the GitHub stars — actually opened the source files and read the code that runs when your application starts?

The honest answer for virtually every engineer working today is: zero.

The Trust Architecture You Didn’t Design

Package managers are abstraction layers over collaboration. They transform the act of “including someone else’s code in your project” from a deliberate decision involving code review, licensing analysis, and security evaluation into a single command that completes in seconds.

npm install fancy-date-picker

That command doesn’t just install fancy-date-picker. It installs every package fancy-date-picker depends on, and every package those packages depend on, recursively. The dependency tree fans out geometrically. You evaluated one package. You installed forty.

This is dependency abstraction: the package manager resolves, downloads, and installs an entire graph of transitive dependencies, presenting the result as a flat node_modules directory that your bundler traverses without discrimination. Your code and the code written by a stranger three dependency levels deep are treated identically.

The abstraction is built on an assumption: that every participant in the dependency graph is trustworthy. That assumption has been tested, and it has failed.

When the Trust Broke: Three Case Studies

event-stream (2018)

event-stream was a popular npm package for working with Node.js streams, downloaded millions of times per week. Its original maintainer, burned out and unpaid, transferred publishing rights to a contributor named right9ctrl who had submitted several helpful pull requests.

right9ctrl added a new dependency: flatmap-stream. Inside flatmap-stream, buried in a minified file, was code that decoded an encrypted payload targeting the Copay Bitcoin wallet application. The malicious code activated only when imported by Copay’s specific build process. It stole wallet credentials and sent them to an attacker-controlled server.

The attack exploited three layers of trust:

  1. Maintainer trust: the community trusted event-stream because it had a known maintainer
  2. Transfer trust: nobody audited the new maintainer’s intentions
  3. Transitive trust: nobody audited flatmap-stream because it was a dependency of a dependency

The malicious code was live for two months before a developer noticed an unusual decoded string during an unrelated investigation.

ua-parser-js (2021)

ua-parser-js parses browser user-agent strings. It’s downloaded 7+ million times per week and used by companies including Facebook, Amazon, and Google. In October 2021, the maintainer’s npm account was compromised, and three malicious versions were published.

These versions installed a cryptocurrency miner on Linux systems and an information-stealing trojan on Windows. The attack lasted four hours before the maintainer regained control. In those four hours, any CI/CD pipeline that ran npm install without a lockfile — or any system that used a floating version range like ^0.7.0 — pulled and executed the malicious code automatically.

The abstraction that failed here was version resolution. ^0.7.0 means “any compatible version from 0.7.0 up.” The package manager treats all versions matching that range as interchangeable. When the attacker published 0.7.29, 0.7.30, and 0.7.31, they slotted perfectly into that range.

colors.js (2022)

This one wasn’t an external attacker. The maintainer of colors.js and faker.js — packages with a combined 25+ million weekly downloads — deliberately sabotaged his own packages. He replaced the code with an infinite loop that printed gibberish to the console, citing frustration with large corporations using his unpaid work.

Every application that depended on colors.js with a floating version range broke on the next install. The attack surface wasn’t a vulnerability in the code or the platform. It was the assumption that a maintainer will always act in the interests of their downstream users.

What These Attacks Share

Every supply chain attack exploits the same abstraction: the package ecosystem as a trusted computing environment. When you type npm install, you’re expressing trust in:

  • The package author’s current intentions
  • The package author’s future intentions
  • The package author’s operational security (passwords, 2FA, machine security)
  • Every transitive dependency author, recursively
  • The npm registry’s integrity
  • The DNS infrastructure between you and the registry
  • The TLS certificate chain validating the registry’s identity

That’s a lot of trust encoded in two words and a package name.

How to Actually Audit

Knowing the risk is useless without knowing what to do about it. Here are concrete actions, ordered from easiest to most thorough.

Level 1: Automated Scanning

# npm's built-in vulnerability scanner
npm audit

# Python equivalent
pip-audit

# More thorough scanning with Snyk or Trivy
npx snyk test
trivy fs --scanners vuln .

These tools check your dependency versions against databases of known vulnerabilities. They catch published CVEs but not undiscovered malicious code. They’re a floor, not a ceiling.

Level 2: Understand What You’re Installing

Before adding a new dependency, examine it:

# See what files a package contains without installing
npm pack fancy-date-picker
tar -tf fancy-date-picker-*.tgz

# Check the package's dependency tree before installing
npm explain fancy-date-picker

# See who published it and when
npm view fancy-date-picker

Look at the package’s size, number of files, and dependency count. A color utility that pulls in 50 dependencies is suspicious. A date library that includes native binaries deserves scrutiny.

Level 3: Read the Code

For critical dependencies — anything that handles authentication, encryption, data access, or runs with elevated privileges — actually read the source:

# Navigate to the installed package source
cd node_modules/critical-auth-package
find . -name "*.js" | head -20
wc -l src/*.js

You don’t need to read every line of every dependency. You need to read the code that handles sensitive operations: where does it make network requests? What does it do with credentials? Does it eval() anything? Does it use child_process?

Level 4: SBOM Generation

A Software Bill of Materials documents every component in your application:

# Generate an SBOM in CycloneDX format
npx @cyclonedx/cyclonedx-npm --output-file sbom.json

# Generate SPDX format
npx spdx-sbom-generator

An SBOM gives you an inventory. When the next ua-parser-js happens, you can immediately check whether you’re affected instead of grepping through node_modules on a Friday night.

Practical Mitigations That Actually Work

Pin Your Dependencies

// package.json — DON'T do this
"dependencies": {
  "express": "^4.18.0",
  "lodash": "~4.17.0"
}

// DO this — exact versions
"dependencies": {
  "express": "4.18.2",
  "lodash": "4.17.21"
}

Exact versions in package.json combined with a committed package-lock.json (or npm-shrinkwrap.json) ensure that every install produces identical node_modules. No attacker-published version will slide in through a range match.

The lockfile is your critical defense. It records the exact version, resolved URL, and integrity hash of every installed package. When npm install runs with a lockfile present, it verifies each download against the recorded hash. npm ci goes further — it refuses to modify the lockfile and fails if the node_modules state would differ from what the lockfile specifies.

Minimize Your Dependencies

Every dependency you add is code you’re responsible for but didn’t write. Before adding a package, ask:

  • Can I write this in less than 100 lines? If yes, write it.
  • Does this package do one thing I need, or twenty things I don’t?
  • How many transitive dependencies does it bring?
  • Is the package actively maintained? By whom?

The is-odd package — a real npm package that checks if a number is odd — has over 500,000 weekly downloads. It depends on is-number. Its source is three lines of non-trivial code. The check n % 2 !== 0 is one line and requires zero dependencies.

Every dependency you don’t add is a dependency that can’t be compromised.

Review Dependency Updates

Don’t blindly run npm update or accept Dependabot PRs without reading the changelog and diff. For minor and patch updates to critical packages, check what changed:

# Compare versions
npm diff [email protected] [email protected]

Use Organizational Policies

For teams and companies, enforce dependency policies:

  • New dependencies require a review, like a code review but for third-party code
  • Run npm audit in CI and fail on high/critical vulnerabilities
  • Generate and store SBOMs with each release
  • Maintain an approved dependency list for sensitive projects
  • Use a private registry that mirrors and caches approved packages

The Paradox Remains

Here’s what makes supply chain security fundamentally different from other security concerns: there’s no clean solution. You can’t build modern software without dependencies. You can’t audit every dependency thoroughly. You can’t trust every dependency blindly.

What you can do is manage the risk consciously instead of ignoring it. Know what’s in your dependency tree. Pin your versions. Lock your installs. Scan for known vulnerabilities. Read the code for critical packages. Minimize your dependency count. Have a process for evaluating new additions.

The package manager abstraction is powerful because it lets you build on the work of thousands of developers. It’s dangerous because it lets you depend on the trustworthiness of thousands of developers. The engineers who understand both sides of that equation — who use dependencies but don’t worship them, who trust selectively rather than universally — are the ones whose applications survive contact with the real world.

Nobody has time to read 1,500 packages. But if you can’t name the ten most critical ones in your dependency tree, you don’t understand your own application’s attack surface. Start there.