Chapter 47 — Supply chain and the code you didn't write
In one sentence
Most of your application is third-party code — dependencies, base images, build tools, CI plugins, CDNs — and each is a path into production that you must inventory, pin, verify, and monitor.
The attack surface
DIRECT DEPENDENCIES the 20 packages you chose
TRANSITIVE DEPENDENCIES the 800 they pulled in (this is where it hides)
BUILD-TIME compilers, bundlers, codegen, CI plugins,
GitHub Actions, pre/post-install scripts
BASE IMAGES FROM node:20 -> a whole OS you didn't audit
DEV TOOLS IDE extensions, linters, test frameworks
RUNTIME PULLS CDN <script>, fonts, analytics, tag managers
INFRASTRUCTURE Terraform modules, Helm charts, Ansible roles
How supply-chain attacks actually happen
TYPOSQUATTING / name confusion reqeusts, python-dateutil vs
python3-dateutil, lodahs. Install-time scripts run as you.
DEPENDENCY CONFUSION publish a PUBLIC package with the same name as a
company's PRIVATE one + higher version -> the resolver picks the
public one (Alex Birsan, 2021 -- hit Apple, Microsoft, dozens more).
COMPROMISED MAINTAINER stolen npm/PyPI creds, or a maintainer goes
rogue (event-stream 2018; ua-parser-js 2021; a burnt-out maintainer
hands over the keys).
MALICIOUS UPDATE to a legit package (xz-utils / liblzma backdoor,
2024 -- a multi-year social-engineering campaign to plant an sshd
backdoor; caught by luck).
BUILD SYSTEM COMPROMISE SolarWinds (2020) -- malware injected into the
build, so the SIGNED official artifact was backdoored.
CI/CD leaked tokens, `pull_request_target` misuse, poisoned caches,
self-hosted runner takeover, a malicious Action pinned to a mutable tag.
PROTESTWARE a maintainer ships sabotage (node-ipc, 2022).
The defences (SLSA-style, layered)
INVENTORY
[ ] Generate an SBOM (CycloneDX / SPDX) per build. Know every component
and version in prod. (syft, cdxgen, `npm sbom`, trivy)
PIN & LOCK
[ ] Commit lockfiles (package-lock.json, poetry.lock, go.sum, Cargo.lock).
[ ] Pin GitHub Actions to a full commit SHA, not @v3 (mutable tag).
[ ] Pin base images by DIGEST (FROM node:20@sha256:...), not tag.
[ ] Disable/participate cautiously in install scripts
(npm --ignore-scripts where feasible; vet the exceptions).
VERIFY
[ ] Signature/provenance verification: npm provenance, PyPI Trusted
Publishing + attestations, Sigstore/cosign for containers, SLSA
provenance, `cargo-vet`/`cargo-crev`.
[ ] Checksum-pin where signatures aren't available.
[ ] Use a PRIVATE REGISTRY / pull-through proxy (Artifactory, GAR,
Verdaccio) with an allowlist; block direct public pulls from prod
builds. Scope internal names to defeat dependency confusion
(npm scopes, explicit index-url, no implicit fallback).
SCAN & MONITOR
[ ] SCA in CI: Dependabot/Renovate, `npm audit`, `pip-audit`,
`osv-scanner`, Trivy, Snyk/Grype -> fail the build on known-critical
CVEs (with a triage/allowlist process so it's not ignored).
[ ] Watch advisories: GitHub Advisory DB, OSV, your ecosystem's feeds.
[ ] Renovate/Dependabot for timely, reviewed updates (stale deps are
the more common real problem than malicious ones).
CONTAIN
[ ] Least privilege for CI: short-lived OIDC creds, no long-lived
secrets, minimal token scopes, protected branches, required
reviews, environment protection rules.
[ ] Reproducible / hermetic builds where you can (SLSA L3+).
[ ] Runtime: minimal/distroless images, non-root, read-only FS,
egress filtering (a backdoored dep still has to call home).
[ ] Subresource Integrity (SRI) for any CDN <script>/<link>, or better,
self-host.
DATA & CODE INTEGRITY (OWASP A08)
[ ] Signed releases (Ch 22/26 tool). Verify before deploy.
[ ] No insecure deserialization of untrusted data (pickle, Java
ObjectInputStream, PHP unserialize, YAML unsafe_load, .NET
BinaryFormatter) -> RCE. Use JSON / schema-validated formats.
[ ] Protect the update channel: signed, versioned, rollback-safe (TUF).
Practice (40 min)
# 1. See how deep the tree really goes
npm ls --all 2>/dev/null | wc -l # or: npm ls --all --json | jq '..|.dependencies?|keys?'
pipdeptree 2>/dev/null | wc -l
go list -m all | wc -l
# 2. Generate an SBOM and scan it
syft dir:. -o cyclonedx-json > sbom.json # or: trivy fs --format cyclonedx -o sbom.json .
grype sbom:sbom.json # or: osv-scanner --sbom sbom.json
trivy fs --scanners vuln,secret,misconfig .
# 3. Audit with the ecosystem tools
npm audit --audit-level=high
pip-audit -r requirements.txt
osv-scanner -r .
cargo audit # if Rust
# 4. Find unpinned CI and images (your repos)
grep -rnE 'uses: .*@(v?[0-9]+|main|master)$' .github/workflows/ # mutable Action refs
grep -rn 'FROM .*:[^@]*$' Dockerfile* # tag, not digest
# 5. Dependency-confusion check: do you have internal package names that
# are NOT scoped / could be resolved from the public registry?
cat package.json | jq '.dependencies|keys[]' | grep -v '^"@' # unscoped deps
# 6. SRI for a CDN script:
# printf '' | openssl dgst -sha384 -binary < jquery.min.js | openssl base64 -A
# <script src="..." integrity="sha384-..." crossorigin="anonymous"></script>
Mini-project (60 min): supply-chain gate for SecureShop
Add to SecureShop's CI a "supply-chain gate" stage that:
1. Builds an SBOM (syft) and stores it as an artifact per release.
2. Runs osv-scanner + trivy; FAILS on CRITICAL/HIGH with no
documented, time-boxed exception file (.security/allow.yml with
reason + expiry + owner).
3. Verifies all GitHub Actions are pinned to SHAs (a lint step).
4. Verifies the base image is pinned by digest.
5. Signs the built container with cosign and generates SLSA
provenance; the deploy step verifies the signature before rollout.
6. Emits a short report to ~/sec-lab/reports/part10-supplychain.md:
dependency count (direct/transitive), findings, exceptions,
what the gate blocked.
Common confusions
- "
npm auditis noise." Much of it is low-severity transitive stuff — but triage it with a process (allowlist + expiry), don't blanket-ignore. The one real RCE will be in there too. - "Lockfile = safe." Lockfiles pin versions (integrity), which is essential, but a pinned version can still be malicious (compromised release). Add signature/provenance verification and scanning.
- "We only use big, popular packages."
event-stream,ua-parser-js,xz-utils, andleft-padwere all popular. Popularity is not safety; maintenance health and provenance are. - "Our code is signed, so the pipeline is secure." SolarWinds signed the backdoored artifact. Protect the build, not just the release step.
- "Pinning Actions to @v3 is pinning." Tags are mutable. Pin to the commit SHA.
- "SCA covers us." SCA finds known CVEs. It won't catch a fresh malicious package or a novel backdoor. Layers: inventory + pin + verify + scan + contain.
Check yourself
- Where in the dependency tree does most hidden risk live, and why?
- Explain dependency confusion and one way to prevent it.
- What is an SBOM and why generate one per build?
- Why pin GitHub Actions and base images by digest/SHA rather than tag?
- What did SolarWinds teach that "sign your releases" alone doesn't cover?
- Name a deserialization function in a language you use that can lead to RCE, and the safe alternative.
Further reading
- Framework: SLSA (
slsa.dev) — supply-chain integrity levels; start with the "get started" and the threats page. - Project: Sigstore / cosign (
sigstore.dev); OSV andosv-scanner(osv.dev); Syft/Grype (Anchore); Trivy (Aqua). - Guidance: OWASP "Software Supply Chain Security" and the CNCF "Software Supply Chain Best Practices" paper; SP 800-204D; CISA/NSA "Securing the Software Supply Chain" series.
- Write-ups: Alex Birsan, "Dependency Confusion" (2021); the
xz-utilsbackdoor analyses (Andres Freund's disclosure + timelines); the SolarWinds SUNBURST technical reports. - Cheat sheet: OWASP "Vulnerable Dependency Management" and "Deserialization".