The idea: your app is mostly other people's code
Software Supply Chain Failures is item A03 of the OWASP Top 10:2025. It grew out of A06:2021, Vulnerable and Outdated Components, and now covers the whole path code takes to production: the packages you depend on, the registries and mirrors they come from, the build system, the artifacts it produces and the deploy step. A typical web app has a handful of direct dependencies and hundreds of transitive ones. Every one of them runs with your app's permissions. An attacker who can change any link of that chain doesn't need a bug in your code.
Weaknesses that fit here (CWE) include:
- CWE-494 Download of Code Without Integrity Check
- CWE-829 Inclusion of Functionality from Untrusted Control Sphere
- CWE-1104 Use of Unmaintained Third Party Components
- CWE-1357 Reliance on Insufficiently Trustworthy Component
- CWE-1395 Dependency on Vulnerable Third-Party Component
Reading the canvas: DEVELOPER & CI is the shop-web repository and the CI runner that installs and builds it, with secrets such as NPM_TOKEN in its environment. The PACKAGE REGISTRY lists the packages and versions on offer; mallory, the attacker, sits at its bottom. BUILD ARTIFACTS & DEPLOY holds the images CI built and the server that runs one of them. The DEPENDENCY TREE below shows what ends up in node_modules. Choose an attack and a pipeline and press Run, or run a demo; each demo runs the attack against the vulnerable pipeline and then the hardened one. express, qs and CVE-2022-24999 are real; log-pretty and str-color are made up. Hashes and signatures are shortened, made-up values.
The dependency tree
shop-web names two packages in package.json. Installing them brings four more: express needs body-parser and qs, and log-pretty needs str-color. Real trees are much bigger, and each node is maintained by someone else: often a single volunteer. Two things make every node dangerous. Its code runs inside your process. And on npm, a package can declare install scripts (preinstall, postinstall) that run as soon as it is installed, on a developer's laptop or a CI runner, before any of your tests.
Typosquatting
An attacker publishes a package whose name is one slip away from a popular one. In 2017 crossenv, a lookalike of cross-env, and dozens of other typosquats on npm sent the installer's environment variables to their author. On the canvas a developer types crossenv, and its postinstall script sends the CI's NPM_TOKEN and AWS key to mallory (Demo 1: typosquatted package).
The hardened pipeline adds dependencies only through a pull request that a dependency-review job checks: how old is the package, how many people use it, does its name sit one edit away from a famous one, does it run install scripts? ignore-scripts=true in .npmrc stops install scripts as a second line. Some teams go further and install only from an internal registry proxy that holds approved packages.
Compromised updates and lockfiles
A trusted package can turn bad. A maintainer's account is phished, or a new "helper" maintainer is given publish rights, and a new version ships with malware. That happened to event-stream in 2018 and ua-parser-js in 2021. In 2024 the xz compression library received a backdoor from a contributor who had worked on it for two years.
package.json usually holds version ranges: "str-color": "^1.3.0" means 1.3.0 or any newer 1.x. Without a lockfile, every npm install resolves the ranges again and takes the newest match, so a version published an hour ago is in the next build (Demo 2: compromised update). The lockfile (package-lock.json) records the exact version of every package in the tree. Commit it, and install in CI with npm ci, which installs exactly what the lockfile says and fails if it doesn't match package.json. Updates then arrive only as pull requests that change the lockfile: from Dependabot or Renovate, reviewed, and ideally after a waiting period (Renovate's minimumReleaseAge) so that a bad release is usually caught and pulled before you take it.
Integrity hashes
A version number names a release, not its bytes. If a mirror, a cache or a proxy between you and the registry serves a different file under the same name, nothing about "1.3.0" changes (Demo 3: tampered tarball). That is why each lockfile entry also has an integrity field, a SHA-512 hash in the same format as Subresource Integrity:
"node_modules/str-color": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/str-color/-/str-color-1.3.0.tgz",
"integrity": "sha512-Qm9x…T4w=="
}
npm hashes every downloaded tarball and compares it with that value. A mismatch stops the install with EINTEGRITY, wherever the file came from. The hash was recorded when the dependency was added and reviewed, so it protects every later build. Other ecosystems have the same idea: go.sum, Cargo.lock, pip's --require-hashes.
SBOM: knowing what you run
When a vulnerability is announced, the first question is "do we use it, and where?". CVE-2022-24999 is a real example: qs before 6.10.3 lets a request with __proto__ keys hang a Node process, and Express before 4.17.3 is affected. package.json doesn't list qs at all, because express brings it in (Demo 4: new CVE).
A Software Bill of Materials lists every component of a build with its exact version, usually as a package URL such as pkg:npm/qs@6.7.0. The two common formats are OWASP CycloneDX and SPDX; npm sbom --sbom-format cyclonedx writes one from the lockfile. Generate it in CI for every build and store it with the artifact. Then "are we affected?" is one query across all services, and the tree shows which direct dependency to upgrade.
Signed builds and provenance
The build system is part of the chain. In the 2020 SolarWinds attack the source code was clean, but code was injected while the product was being built, and the signed result went to thousands of customers. In 2021 a modified Codecov uploader script read secrets from the CI jobs that ran it. A simpler hole is the artifact store: anyone with a push token can upload an image under a new tag, and a deploy that runs "the newest tag" runs it (Demo 5: forged build artifact).
The fix is to sign every artifact in CI and verify the signature before deploying. With Sigstore's cosign the CI job signs with a short-lived certificate tied to its workflow identity, so there is no long-lived signing key to steal, and the deploy step runs cosign verify --certificate-identity …/build.yml@refs/heads/main. A provenance statement (the SLSA framework defines its levels) says which commit and which build produced the artifact. The same idea applies to packages: npm publish --provenance links a package to the build that made it, and npm audit signatures checks it. See How a Digital Signature Works for what the signature itself proves.
How to prevent it
| Link of the chain | Attack | Check |
|---|---|---|
| New dependency | Typosquat, abandoned or tiny package | Review on PR: age, users, maintainers, name; fewer dependencies |
| Updates | Hijacked maintainer account | Committed lockfile, npm ci, updates as reviewed PRs after a waiting period |
| Download | Swapped file on a mirror or proxy | Lockfile integrity hashes |
| Install | Malicious install script | ignore-scripts, no secrets in the install step |
| Inventory | Known CVE nobody knows you run | SBOM per build, continuous scanning (npm audit, OWASP Dependency-Track) |
| Build | Poisoned build step, stolen CI secrets | Isolated, ephemeral runners; least-privilege tokens; reviewed pipeline config |
| Deploy | Forged artifact | Signature and provenance verified before it runs |
OWASP Top 10:2025 series
- A01 Broken Access Control
- A02 Security Misconfiguration
- A03 Software Supply Chain Failures (this page)
- A04 Cryptographic Failures
- A05 Injection
- A06 Insecure Design
- A07 Authentication Failures
- A08 Software or Data Integrity Failures
- A09 Security Logging and Alerting Failures
- A10 Mishandling of Exceptional Conditions
See also OWASP A08: Software or Data Integrity Failures, How a Digital Signature Works and Encryption vs Hashing.
What the page leaves out
Real hashes and signatures (the canvas shows shortened, made-up ones); version resolution and deduplication in detail; peer and optional dependencies; dependency confusion between a private and a public registry; vendoring; reproducible builds; Sigstore's Fulcio and Rekor transparency log; SLSA levels; VEX documents that say whether a listed CVE is actually reachable; and the licence side of an SBOM.