awesome-everything RU
↑ Back to the climb

Security

Supply-chain security: your real attack surface is your dependencies

Crux You ship far more third-party code than your own, so the modern attack surface is the install step. Lockfiles, pinned versions, provenance, and registry precedence are the defenses against xz-style backdoors and dependency confusion.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at junior altitude — the surface
◷ 16 min

A Microsoft engineer was benchmarking PostgreSQL on a test box in March 2024 when SSH logins started taking 500ms instead of the usual ~100ms. That half-second was the only visible symptom of CVE-2024-3094: a backdoor planted in xz-utils, a compression library sitting underneath sshd on most Linux distributions. The code was clean. Nobody wrote a vulnerable line. An attacker had spent roughly two years becoming a trusted maintainer of an obscure dependency, then shipped a backdoor through the build. It was caught by a latency regression, not a code review.

You ship mostly other people’s code

A modern web app is a thin layer of your own logic floating on a deep stack of dependencies. A fresh create-next-app or NestJS project pulls in hundreds of packages, the vast majority transitive — installed by your dependencies, not by you, and never named in your package.json. The honest framing for a senior: most of the bytes you ship to production were written by strangers, audited by no one on your team, and updated automatically.

That changes where attacks land. Cross-site scripting and SQL injection target your code. Supply-chain attacks skip your code entirely and target the install step — the moment your machine or CI fetches a package and runs its install scripts with your credentials, your registry tokens, and your network access. You can write flawless application code and still get owned at npm install.

How the real incidents worked

Four documented attacks map the surface. Each justifies a specific defense.

The xz-utils backdoor (CVE-2024-3094, 2024) was a multi-year social-engineering campaign. A contributor named “Jia Tan” spent ~2021–2024 building trust on the xz project — patches, helpfulness, sock-puppet accounts pressuring the burned-out original maintainer — until they became a co-maintainer. Then they hid malicious code in obfuscated binary test files and a tweaked build script, so the backdoor existed only in the released tarball, not in the readable Git source. It targeted sshd auth and reached Fedora, Debian unstable, and Kali before a 500ms latency tell exposed it. Lesson: trust in a maintainer is not trust in a build.

Dependency confusion (Alex Birsan, 2021) was simpler and scarier. Many package managers, when you ask for a package, check the public registry as well as your private one — and prefer the higher version number. Birsan harvested internal package names leaked in public JS bundles, published packages with those exact names to npm/PyPI/RubyGems at version 9.9.9, and waited. Builds at Apple, Microsoft, Tesla, Uber and 30+ others silently resolved his public package over their internal one and ran his code. He earned over $130,000 in bug bounties for proving it.

Typosquatting banks on a fat finger: a package named lodahs or crossenv sitting next to the real lodash or cross-env. One typo in a Dockerfile and you’ve installed the attacker’s package.

Compromised legit packagesevent-stream (2018) and ua-parser-js (2021) — show the third path: a real, widely-used package gets a malicious version. event-stream got a new “maintainer” who added a payload targeting a specific crypto wallet; ua-parser-js, with tens of millions of weekly downloads, was hijacked to ship a cryptominer and credential stealer for a few hours.

AttackMechanismPrimary defense
xz-utils backdoorTrusted maintainer ships backdoor in build artifact, not sourceBuild provenance (SLSA), reproducible builds, signed artifacts
Dependency confusionPublic package shadows internal name at higher versionScoped names + registry precedence; reserve internal names publicly
TyposquattingLookalike name next to a popular oneLockfile + review of new deps; —ignore-scripts
Compromised packageReal popular package gets a malicious versionPinned versions + integrity hashes; delayed adoption; audit

The base layer: lockfiles, integrity, and npm ci

A lockfile (package-lock.json, yarn.lock, pnpm-lock.yaml) pins every dependency — direct and transitive — to an exact version and a cryptographic integrity hash (sha512-...). The version stops a silent jump to a malicious release; the hash means a package whose bytes changed under the same version number fails the install. This is your first and cheapest line of defense.

But the lockfile only protects you if you actually enforce it. npm install will happily mutate the lockfile to satisfy a loosened range; npm ci does the opposite — it installs exactly the lockfile and errors if package.json and the lock disagree. The senior rule: npm ci in CI, never npm install. Add --ignore-scripts to stop arbitrary postinstall code from running during the fetch, which is how most install-time payloads execute. Pinning exact versions (no ^ ranges) plus a short adoption delay — don’t take a release the day it ships — blunts the ua-parser-js-style window where a malicious version is live for hours before it’s pulled.

Why this works

Why hashes and not just version pins? A version number is a label the publisher controls; bytes are not. If an attacker republishes the same version with new content (or a registry is compromised), a version-only pin happily installs it. The integrity hash is computed from the actual tarball, so any change — malicious or accidental — breaks the install loudly instead of running silently.

The integrity layer: SBOM, provenance, and signing

Lockfiles tell you what you depend on; they say nothing about how those artifacts were built or whether they were tampered with after publication. That’s the gap xz exploited — the source was clean, the shipped artifact was not.

An SBOM (Software Bill of Materials) is the ingredient list: a machine-readable inventory of every component and version in a build, in a standard format like CycloneDX or SPDX. When the next CVE drops, an SBOM answers “are we affected?” in seconds instead of a frantic grep across repos. SLSA (Supply-chain Levels for Software Artifacts) is the complementary half — it certifies how the artifact was built. Its progressive levels go from “provenance exists” (L1) up to a hardened, isolated, non-falsifiable build (L3); most teams target L2–L3 for production. Provenance is the signed, verifiable record linking an artifact back to the exact source commit and build that produced it, so a tampered binary can’t masquerade as official. Signed commits and signed artifacts (via Sigstore / in-toto attestations) close the loop: you verify a signature before you trust the bytes, instead of trusting a name.

LayerQuestion it answersTooling
Lockfile + hashDid I get the exact bytes I expected?npm ci, integrity field
SBOMWhat components are in this build?CycloneDX, SPDX, Syft
Provenance (SLSA)How and from what source was it built?SLSA attestations, in-toto
SigningIs this artifact authentic and untampered?Sigstore, signed commits

Stopping dependency confusion: precedence, not luck

Confusion attacks exploit resolution order. The fix is to make your build never reach the public registry for an internal name. Use a scoped namespace (@acme/billing) that you own on the public registry, so no one can publish under it. Configure your package manager so the internal/private registry has precedence for your scope, and either mirror public packages through one proxy or block the public registry for internal scopes entirely. As a belt-and-braces move, publish placeholder packages under your internal names to the public registry yourself — owning the name is the simplest way to ensure no attacker can. Automated dependency review (Dependabot, npm audit, Renovate) then watches for known-vulnerable versions and flags risky new dependencies in the PR, before they ever reach the lockfile.

Pick the best fit

Your CI pipeline runs `npm install` and uses caret ranges in package.json. A teammate proposes hardening the install step. Pick the strongest change.

Quiz

What single observation led to the xz-utils backdoor (CVE-2024-3094) being discovered?

Quiz

Why does a dependency-confusion attack succeed even when your internal package has a valid version?

Order the steps

Order the defenses from cheapest/most-immediate to most organizational:

  1. 1 Commit a lockfile and switch CI to `npm ci` with integrity hashes
  2. 2 Pin exact versions and add `--ignore-scripts` to installs
  3. 3 Enable automated dependency review (Dependabot / audit) on every PR
  4. 4 Use scoped names + private-registry precedence; reserve internal names publicly
  5. 5 Generate SBOMs and require SLSA provenance + signed artifacts for releases
Recall before you leave
  1. 01
    A teammate says 'we pin versions in package.json, so we're safe from supply-chain attacks.' Explain what that does and does not protect against.
  2. 02
    Walk through how a dependency-confusion attack works and the layered defense against it.
Recap

The code you author is a thin layer on a deep stack of dependencies you didn’t write, so the modern attack surface is the install step, not your application logic. The documented incidents map the surface: xz-utils showed that a trusted maintainer can ship a backdoor in a build artifact whose source looks clean; dependency confusion showed that public registries can shadow internal names at a higher version; typosquatting and the event-stream / ua-parser-js compromises showed how a lookalike or a hijacked-but-real package slips in. The defenses layer up. The base layer is a committed lockfile with integrity hashes, installed via npm ci (never bare npm install), with exact pins and --ignore-scripts. Above that, automated dependency review catches known-bad versions, scoped names plus private-registry precedence kill confusion, and SBOMs plus SLSA provenance and signed artifacts answer the questions hashes can’t: what’s inside the build and whether it was tampered with after the source was written. None of it is exotic — it’s mostly configuration you turn on before the next 500ms surprise finds you.

Continue the climb ↑Supply-chain security: multiple-choice review
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.