Skip to content

Supply Chain Attacks

A supply chain attack targets the software or hardware delivery pipeline rather than the end organisation directly. Instead of attacking a hardened target head-on, the attacker compromises a trusted component - a software library, a build tool, a vendor’s update mechanism, or a hardware component - that the target organisation trusts and uses. The compromise then propagates silently to thousands of downstream organisations.


Traditional attack path:
Attacker → Target (hardened, monitored, defended)
→ High friction; often detected
Supply chain attack path:
Attacker → Supplier (smaller, less defended) → Trusted update/library
→ Target receives the compromised component AUTOMATICALLY
→ Target's own security controls approve the delivery
→ Detection may take months
Multiplier effect:
Compromise 1 supplier → gain access to N customers
SolarWinds: 1 build server compromise → ~18,000 customer networks

The canonical supply chain attack.

Attack chain:
1. Attackers (APT29 / Cozy Bear) compromised SolarWinds' build environment
2. Injected SUNBURST backdoor code into Orion IT monitoring software
3. Trojanised update (Orion 2019.4 - 2020.2.1) was signed by SolarWinds'
legitimate code signing certificate
4. ~18,000 organisations installed the update (including US Treasury,
CISA, FireEye, Microsoft, and most major US federal agencies)
5. SUNBURST lay dormant for 12-14 days before activating
(to evade sandbox analysis that times out early)
6. When active: exfiltrated data, enabled lateral movement
Why it worked:
- Trusted source (legitimate SolarWinds update)
- Legitimate code signing certificate
- Long dormancy period
- Communicated via legitimate-looking Orion API channels
- Attacker lived off the land - used existing tools, not novel malware
Detection took 6-9 months. FireEye noticed their own red team tools were stolen.

Supply chain attack targeting a VoIP software vendor:

Attack chain:
1. 3CX developers had ffmpeg library compromised on THEIR systems
(via a Trojanised trading application update - another supply chain event)
2. Malicious ffmpeg caused 3CX desktop app builds to include a backdoor
3. 3CX signed and distributed the trojanised installer to ~600,000 customers
4. Customers' security tools initially trusted the signed 3CX binary
Key detail: This was a supply chain attack of a supply chain attack.
Attack chain:
1. Malicious actor "Jia Tan" spent 2+ years building trust in the
XZ Utils open-source project (commit history, bug reports, code reviews)
2. Gained maintainer access through social engineering of the original
maintainer (who was overwhelmed and burned out)
3. Injected a backdoor into liblzma 5.6.0 and 5.6.1 that specifically
targeted sshd via systemd on glibc Linux systems
4. The backdoor would allow authentication bypass in sshd
(RCE as sshd on affected systems)
Caught accidentally by Microsoft engineer Andres Freund noticing
unexpected CPU usage during SSH login while benchmarking.
Estimated 3-4 weeks from near-global Linux distribution before discovery.

A 2021 attack technique exploiting how package managers resolve private package names:

How it works:
1. Company uses a private npm package: @company/internal-utils (v1.0.0)
2. Attacker publishes public npm package with SAME NAME: @company/internal-utils (v2.0.0)
3. npm resolves by highest version number → installs attacker's v2.0.0
4. Attacker's package executes on developer machines and CI/CD servers
Real impact:
Security researcher Alex Birsan used this against Apple, Microsoft, Uber,
Tesla, PayPal, Netflix, and others - all paid bug bounties
Affected:
- npm (JavaScript)
- PyPI (Python)
- RubyGems (Ruby)
- Go modules
- NuGet (.NET)

An SBOM is a machine-readable inventory of all software components in a product - every library, dependency, and version.

Why SBOMs matter:
Without SBOM: "We don't know if Log4Shell affects us."
With SBOM: Query in 30 seconds: grep log4j sbom.json
→ Immediately know every product, version, and deployment
SBOM formats:
SPDX (Linux Foundation standard; common in government)
CycloneDX (OWASP standard; widely tooled)
SWID (ISO standard; primarily for installed software)
Terminal window
# Generate an SBOM for a Python project
pip install cyclonedx-bom
cyclonedx-py environment --output sbom.json # from installed packages
cyclonedx-py poetry --output sbom.json # from poetry.lock
# Generate SBOM for Node.js
npx @cyclonedx/cyclonedx-npm --output sbom.json
# Generate SBOM for a Docker image (Syft)
apt install syft # or: curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh
syft myimage:latest -o cyclonedx-json > image-sbom.json
syft myimage:latest -o spdx-json > image-sbom-spdx.json
# Scan the SBOM for known vulnerabilities (Grype)
grype sbom:image-sbom.json
# Or scan directly
grype myimage:latest
grype dir:/path/to/project
Terminal window
# Check if your SBOM contains a vulnerable component (e.g., Log4j)
cat sbom.json | python3 -c "
import json, sys
sbom = json.load(sys.stdin)
for component in sbom.get('components', []):
name = component.get('name', '')
version = component.get('version', '')
if 'log4j' in name.lower():
print(f'Found: {name} {version}')
"
# Anchore Grype: scan image and check specific CVE
grype myimage:latest | grep CVE-2021-44228 # Log4Shell
# OSV (Open Source Vulnerabilities) database query
curl -s 'https://api.osv.dev/v1/query' \
-d '{"package":{"name":"log4j","ecosystem":"Maven"},"version":"2.14.1"}' \
| python3 -m json.tool | grep -E '"id"|"summary"' | head -20

Terminal window
# Verify an RPM package signature
rpm --import https://www.redhat.com/security/team/key/RPM-GPG-KEY-redhat-release
rpm -K package.rpm # should say: sha1 md5 OK
# Verify a Debian package (apt handles this automatically, but check manually)
apt-key list # list trusted keys
dpkg-sig --verify package.deb
# Verify a Docker image signature (cosign)
apt install cosign
cosign verify --certificate-identity-regexp='.*' \
--certificate-oidc-issuer-regexp='.*' \
cgr.dev/chainguard/nginx:latest
# Verify a Python package (via pip hash checking)
# requirements.txt with hashes (pip-compile generates these)
pip install --require-hashes -r requirements.txt
# requirements.txt format:
# requests==2.28.0 \
# --hash=sha256:e72d28c4...
# --hash=sha256:b3559a13...

Software Composition Analysis (SCA) in CI/CD

Section titled “Software Composition Analysis (SCA) in CI/CD”
# GitHub Actions: automatic dependency vulnerability scanning
name: Security Scan
on: [pull_request, push]
jobs:
sca:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
# Dependabot: built into GitHub (enable in .github/dependabot.yml)
# Snyk: commercial SCA
- name: Snyk scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high
# OWASP Dependency-Check
- name: OWASP Dependency Check
uses: dependency-check/Dependency-Check_Action@main
with:
project: 'MyProject'
format: 'HTML'
out: 'reports'
Terminal window
# npm audit (built-in)
npm audit # list vulnerabilities
npm audit --audit-level=high # fail CI if any high+ vulns
# pip-audit (Python)
pip install pip-audit
pip-audit # scans installed packages
# GitHub Dependabot configuration (.github/dependabot.yml)
cat << 'EOF' > .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
labels:
- "dependencies"
- "security"
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
EOF
Terminal window
# Reproducible builds: same source → same binary (deterministic compilation)
# Allows independent parties to verify the binary matches the source
# Verify a reproducible build (Debian example)
apt install reprotest
reprotest 'dpkg-buildpackage -b -uc -us' '..'
# Check if Python wheels are reproducible
# pip install reprolock
# reprolock verify
# GitHub Actions: pin all action versions to a specific commit SHA
# ❌ Vulnerable (floating tag - attacker can push new code to v3)
uses: actions/checkout@v3
# ✅ Secure (pinned to immutable SHA)
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v3.5.3
# Use Dependabot to auto-update pinned SHAs
Principle of least surprise in CI/CD:
✅ Use isolated build environments (fresh VM/container per build)
✅ No internet access during build (except approved registries)
✅ Private package registry mirror (proxy npm/PyPI to internal registry)
✅ Code signing for all release artifacts
✅ SBOM generated and stored with every release
✅ Reproducible builds where possible
✅ Separate signing environment from build environment
✅ Multi-person approval for builds that go to production
✅ Monitor supply chain: Dependabot / Renovate / pip-audit in CI
❌ Avoid: downloading arbitrary scripts with curl | sh
❌ Avoid: build steps that pull from uncontrolled external sources
❌ Avoid: dev dependencies available in production builds
❌ Avoid: npm/pip installs without lockfiles and hash pinning

Terminal window
# npm: Scope packages to prevent confusion
# Use --scope in .npmrc to ensure scoped packages use private registry
cat > .npmrc << 'EOF'
@company:registry=https://npm.pkg.github.com
always-auth=true
EOF
# For all packages, pin to private registry:
echo "registry=https://registry.internal.example.com" >> .npmrc
# pip: Use --index-url to restrict to private PyPI
pip install --index-url https://pypi.internal.example.com/simple/ mypackage
# Or in pip.conf:
echo "[global]
index-url = https://pypi.internal.example.com/simple/
extra-index-url = https://pypi.org/simple/
" > ~/.pip/pip.conf
# Note: extra-index-url still allows public packages - vulnerable to dependency confusion
# Safer: use ONLY the private registry (which mirrors what you need from public PyPI)
# Go: use GOPROXY to control module resolution
export GOPROXY=https://goproxy.internal.example.com,direct
export GONOSUMCHECK=*.internal.example.com