Dependencies and the supply chain

Fifteen declared packages become 515, and four starters become 61 jars — each running with your privileges. Reachability, lockfiles, and the question Log4Shell was really about.

7 min read🛡️ Application Security

Most of the code in your application is not yours, and you did not read any of it. That is not a criticism — it is the only way anything ships — but it means your security depends on a supply chain, and the shape of that chain is worth knowing exactly.

Count what you did not choose

This project's package.json declares 15 dependencies, thirteen of them runtime. What is actually installed:

plaintext
###   total declared:  15
###   total packages in the tree: 515

Thirty-four times what was chosen. On the Java side, a service declaring four things — the web, data-jpa and validation starters, plus a database driver — gets:

plaintext
### jars on the classpath: 61
### a sample of what you did not choose:
###   antlr:antlr
###   ch.qos.logback:logback-classic
###   com.fasterxml.jackson.core:jackson-databind
###   com.fasterxml:classmate

A parser generator, a logging framework, six Jackson artifacts. Every one of them runs inside your process, with your privileges, with access to your environment variables and your database connection. A vulnerability in any of them is a vulnerability in your service, and a malicious one is game over.

That is the whole problem in one sentence: you audited four decisions and accepted sixty-one.

A CVE is not automatically a vulnerability

The OWASP lesson made this point with this project's own four high-severity advisories, accepted rather than fixed because mysql2 arrives through the Prisma CLI and this project uses PostgreSQL. Reachability decides, and the same lesson's other half applies here: you have to establish that, not assume it.

What makes an advisory actually matter, roughly in order:

  1. Is the vulnerable code on a path your application executes? A CVE in a CLI's sub-dependency that never loads at run time is different from one in your JSON parser.
  2. Can an attacker reach it with input they control? A deserialisation flaw in a library you only feed your own config is not the same as one behind a public endpoint.
  3. Is it a dev dependency or shipped? Build-time code still matters — see the pipeline section below — but the exposure is different.
  4. What would exploitation actually cost you?

Write the answer down when you accept one. An unexamined advisory and a deliberately accepted one look identical in a scanner, and the difference is everything.

The lockfile is the integrity control

A version range says anything compatible. A lockfile says exactly this, and here is its hash:

plaintext
### is the lockfile committed?
###   tracked: package-lock.json
###   entries in it: 573
### does it pin integrity hashes?
###   572 of 572 entries carry an integrity hash

Every entry carries a sha512 of the package contents. If a published version is ever altered, the install fails rather than succeeding quietly — which is the defence against a registry being compromised or a maintainer replacing a release in place.

Two rules follow, and both are commonly broken:

  • Commit the lockfile. Without it, two machines resolve different trees and "works on my machine" becomes a security statement as well as a reliability one.
  • npm ci, not npm install, in CI and in deployment. install may update the lockfile to satisfy a range; ci installs exactly what is locked and fails if the lockfile and manifest disagree. This project's deploy runs npm ci --omit=dev for that reason.

Maven's equivalent is discipline rather than a file: never a version range, never a SNAPSHOT in a release, and a BOM to pin what you did not declare. The JPA course's build lesson covers dependencyManagement, which is how you pin a transitive without depending on it directly. npm's version of that is overrides.

The attacks that are not accidents

A vulnerable dependency is a mistake somebody made. These are deliberate:

  • Typosquatting. A package named one character away from a popular one. Read the name in the diff when a dependency is added; that is the only moment anyone looks.
  • Dependency confusion. Your internal package acme-billing is not on the public registry — so somebody publishes one there with a higher version number, and a misconfigured resolver prefers it. The fix is configuration: scope internal packages and pin the registry that may serve them.
  • A compromised maintainer account. A real package, a real name, a malicious new version. This has happened repeatedly to widely-used packages, and the defence is not vigilance — it is the lockfile plus not upgrading blindly.
  • Install scripts. postinstall runs arbitrary code on every developer machine and every CI runner, before any of your tests. npm ci --ignore-scripts is worth knowing about, with the caveat that some legitimate packages need them.

Maven has less of this — Central requires signed artifacts and does not allow republishing a version — but a plugin in your build runs with the same freedom as any install script.

The pipeline is part of the surface

The place teams under-defend, because it does not feel like production:

  • CI has your secrets. A deploy key, a registry token, a cloud role. A build that can be influenced by a pull request from a fork is a build an attacker can influence.
  • Pin your actions and images. actions/checkout@v4 is a moving tag; a digest is not. The same goes for FROM node:22 in a Dockerfile.
  • Least privilege for the build. A CI job that only runs tests does not need a publish token.
  • Reproducible builds. If the same commit produces a different artifact on different days, you cannot say what you deployed — which is the wrapper-and-pinning discipline from the Maven lesson, in a security frame.

An SBOM says what you shipped

A software bill of materials — CycloneDX or SPDX, generated at build time — is the list of everything in an artifact, with versions.

Its value is not compliance, although that is often what funds it. Its value is the morning a severe vulnerability is announced and the only question that matters is "are we affected, and where?" Without an SBOM that is an archaeology project across every service. With one it is a query.

That is the real lesson of Log4Shell, and it is not the one usually drawn. The vulnerability was severe, but what made it a weeks-long incident for most organisations was that nobody could quickly answer where log4j was — it arrived transitively, in services nobody associated with logging.

What to actually do

In order of value per hour spent:

  1. Automate the upgrades. Dependabot or Renovate, with grouped minor updates and CI proving them. Being current is the defence that works before an advisory exists — because the emergency upgrade is the one you cannot do quickly.
  2. Scan in CI and fail on what matters. npm audit, mvn dependency-check, Snyk, Trivy for images. Choose the severity that blocks, and record accepted advisories with a reason and a review date.
  3. Commit lockfiles and install with ci.
  4. Generate an SBOM with each build and keep it with the artifact.
  5. Reduce the surface. The cheapest dependency is the one you do not add. A left-pad is not worth 515 packages of ancestry, and a starter you use one class from is worth a second look.
Progress is saved on this device and to your account when signed in.