Appearance
Supply Chain Security
Published: March 31, 2026
Your dependencies are the largest attack surface you don't control. A single compromised package in your dependency tree — direct or transitive — can steal credentials, install backdoors, or mine cryptocurrency on your CI runners. This guide covers practical defenses for the two most targeted ecosystems (npm and pip) and the CI/CD layer that ties them together (GitHub Actions).
Why This Matters
The npm registry hosts over 2 million packages. PyPI has over 500,000. You install a handful directly — but each one pulls in a tree of transitive dependencies you never audited. Attackers know this. Supply chain attacks have grown from rare, sophisticated operations to a routine tactic.
The pattern is almost always the same: compromise a package maintainer's account, push a malicious version, and wait for automated installs to pull it in. The window of exposure can be as short as four hours — but that's enough when millions of installs happen daily.
1. Real-World Incidents
These are not hypothetical risks. Every entry below happened.
npm
| Incident | Date | Impact |
|---|---|---|
event-stream — attacker social-engineered maintainership, added crypto-stealing payload via flatmap-stream | Nov 2018 | Targeted Copay Bitcoin wallet |
eslint-scope — maintainer account hijacked (password reuse, no 2FA), postinstall exfiltrated .npmrc tokens | Jul 2018 | Triggered npm's push for mandatory 2FA |
| ua-parser-js — account hijacked, cryptominer + password stealer in versions with 8M weekly downloads | Oct 2021 | CISA advisory issued, 4-hour window |
| colors.js / faker.js — maintainer deliberately sabotaged own packages (protestware) | Jan 2022 | 23M weekly downloads disrupted |
Axios — account takeover injected RAT dropper via plain-crypto-js, self-deleting after execution | 2025 | 300M weekly downloads exposed |
| chalk/debug ecosystem — phished maintainer led to compromise of 18 packages totaling 2.6B weekly downloads | Sep 2025 | Largest npm compromise by volume |
PyPI
| Incident | Date | Impact |
|---|---|---|
| ctx — expired email domain hijacking gave attacker account reset, exfiltrated AWS credentials | May 2022 | ~2,000 daily downloads for 10 days |
| Ultralytics YOLO — GitHub Actions script injection stole PyPI upload token, published cryptominer | Dec 2024 | 80M monthly downloads |
| litellm — malicious versions with multi-stage credential stealer, live for 2+ hours | Mar 2025 | 3M daily downloads |
Dependency Confusion
In February 2021, researcher Alex Birsan published public packages with the same names as internal packages at Apple, Microsoft, Uber, Tesla, and Shopify — but with higher version numbers. Package managers defaulted to the higher-versioned public package. He earned $130,000+ in bug bounties across npm, PyPI, and RubyGems simultaneously.
WARNING
Most of these attacks exploited install scripts (postinstall hooks) as the delivery mechanism. A single .npmrc setting — ignore-scripts=true — would have blocked the majority of them.
2. npm Security
Audit for Known Vulnerabilities
bash
# Scan for known CVEs
npm audit
# Production dependencies only (skip devDependencies)
npm audit --omit=dev
# Auto-fix with semver-compatible updates
npm audit fix
# JSON output for CI pipelines
npm audit --jsonDANGER
npm audit fix --force ignores semver constraints and can install breaking major version bumps. Never run it blindly. Prefer npm audit fix (no --force) and manually address remaining issues.
Block Install Scripts
Install scripts (preinstall, postinstall) are the primary delivery mechanism for npm malware. Block them by default.
ini
ignore-scripts=trueThe trade-off: some legitimate packages need postinstall scripts for native module compilation (node-gyp). When you hit one, run its scripts explicitly:
bash
# Allow scripts for a specific package
npm rebuild <package-name>For a more granular approach, use @lavamoat/allow-scripts to maintain an allowlist of packages permitted to run lifecycle scripts.
Use npm ci in CI/CD
bash
# Development: may modify lockfile
npm install
# CI/CD: strict lockfile enforcement
npm cinpm ci deletes node_modules and installs exactly what the lockfile specifies. It never modifies the lockfile. It errors if package.json and package-lock.json are out of sync. Always use npm ci in pipelines.
Override Vulnerable Transitive Dependencies
When a vulnerability exists deep in your dependency tree and the direct dependency hasn't released a fix:
json
{
"overrides": {
"vulnerable-transitive-dep": ">=1.5.3"
}
}Scope overrides to a specific parent when the vulnerable package appears in multiple places:
json
{
"overrides": {
"some-package": {
"vulnerable-transitive-dep": ">=1.5.3"
}
}
}Available since npm v8.3.0. Always test after applying overrides — the parent package may not be compatible with the forced version.
Verify Package Provenance
bash
# Verify signatures on installed packages
npm audit signaturesProvenance links a published package to its source repository and CI/CD build via Sigstore. Look for the "Provenance" badge on npmjs.com package pages. When publishing your own packages:
bash
# Publish with provenance (from GitHub Actions or GitLab CI)
npm publish --provenanceHarden .npmrc
ini
# Block lifecycle scripts by default
ignore-scripts=true
# Pin to official registry (prevents dependency confusion)
registry=https://registry.npmjs.org/
# Scope private packages to your org registry
@yourorg:registry=https://npm.yourcompany.com/
# Save exact versions (no ^ or ~ ranges)
save-exact=true
# Enforce lockfile
package-lock=true
# Audit on every install
audit=true
# Minimum severity threshold for audit failures
audit-level=highProtect Your npm Account
bash
# Enable 2FA for publishing AND login
npm profile enable-2fa auth-and-writes
# Create read-only CI tokens
npm token create --read-only
# List active tokens
npm token list
# Revoke a compromised token immediately
npm token revoke <token-id>Watch for npx Typosquatting
npx some-package downloads and executes a package if it's not installed locally. A typo like npx creat-react-app (missing an "e") could execute a malicious package.
bash
# Require explicit confirmation before downloading
npx --no some-package
# Or use npm exec with separator
npm exec -- some-packageAvoid Recently Published Packages
Most malicious packages are detected and removed from registries within days — but the damage happens in those first hours. Treat any package published less than seven days ago as untrusted.
Before adding a new dependency:
bash
# Check when a package was last published
npm view <package> time --jsonWhat to look for:
- Created date — a brand-new package with no history is higher risk than one with years of releases
- Latest version age — if the latest version was published today and the previous was months ago, investigate why
- Maintainer changes — a new maintainer pushing a release shortly after gaining access is the classic account takeover pattern
WARNING
This is a team policy, not a technical control — npm has no built-in setting to enforce it. Tools like Socket flag recently published packages automatically. Use socket npm install or the Socket GitHub App to catch these before they enter your dependency tree.
For automated enforcement in CI, query the npm registry API and fail the build if any dependency was published within your quarantine window:
bash
# Check age of a specific package version
published=$(npm view <package> time.<version> --json | tr -d '"')
age_days=$(( ($(date +%s) - $(date -d "$published" +%s)) / 86400 ))
if [ "$age_days" -lt 7 ]; then
echo "WARN: <package>@<version> is only $age_days days old"
exit 1
fi3. pip Security
Audit for Known Vulnerabilities
pip-audit is maintained by PyPA (Python Packaging Authority) and Trail of Bits. It checks installed packages against the Python Packaging Advisory Database (OSV).
bash
# Install
pip install pip-audit
# Audit current environment
pip-audit
# Audit a requirements file
pip-audit -r requirements.txt
# Auto-fix (installs minimum safe version)
pip-audit --fix
# JSON output for CI
pip-audit -f jsonHash-Verified Installs
Hash checking mode ensures every installed package matches a known-good hash. This prevents tampered packages and man-in-the-middle attacks.
bash
pip install --require-hashes -r requirements.txtRequirements file with hashes:
text
requests==2.31.0 \
--hash=sha256:942c5a758f98d790eaed1a29cb6eefc7f0d329b5e0be51dc41b9a15f8c95c2a5 \
--hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003e
certifi==2024.2.2 \
--hash=sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1Key rules:
- Hash checking is all-or-nothing — if one package has a hash, all must
- All requirements must be pinned with
==(no ranges) - Use
sha256or stronger - Generate hashes automatically with
pip-compile --generate-hashes(frompip-tools)
Force Virtual Environments
Never install packages into your system Python. Enforce this globally:
bash
pip config set global.require-virtualenv trueOr in your pip config file:
ini
[global]
require-virtualenv = trueini
[global]
require-virtualenv = trueThis makes pip install fail unless you're inside an active virtual environment.
Use Constraints Files
Constraints restrict which versions of transitive dependencies can be installed without declaring them as direct dependencies. Useful for organization-wide security policies.
bash
pip install -c constraints.txt -r requirements.txttext
urllib3==2.1.0
cryptography==42.0.0
setuptools>=70.0.0Harden pip.conf
ini
[global]
# Only install inside virtual environments
require-virtualenv = true
# Pin to official PyPI
index-url = https://pypi.org/simple/
# Timeout for network operations
timeout = 30WARNING
The trusted-host setting disables TLS verification for that host. Never add pypi.org as a trusted host. Only use it for internal registries where you control the network.
Use Isolated Mode in Scripts
bash
# Ignore all config files and environment variables
pip install --isolated -r requirements.txtThis prevents a malicious pip.conf placed in the project directory from redirecting installs to an attacker-controlled registry.
4. Lock File Security
Lock files are your single most important defense against supply chain drift.
Why Lock Files Matter
Without a lockfile, npm install or pip install resolves the latest compatible version at install time. If an attacker publishes a malicious version of a transitive dependency between two installs, the second install gets the compromised version silently.
Lock files prevent this by recording:
- Exact resolved version of every dependency (direct and transitive)
- Integrity hash (SHA-512 in npm) of each package tarball
- Resolved registry URL for each package
Always Commit Your Lock Files
| Package Manager | Lock File | Commit It? |
|---|---|---|
| npm | package-lock.json | Yes |
| Yarn | yarn.lock | Yes |
| pnpm | pnpm-lock.yaml | Yes |
| pip-tools | requirements.txt (compiled) | Yes |
| Poetry | poetry.lock | Yes |
| Pipenv | Pipfile.lock | Yes |
Not committing the lockfile means every install is a gamble.
Integrity Verification
Each entry in package-lock.json includes an integrity field:
json
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
}When npm ci installs packages, it recalculates the hash of the downloaded tarball and compares it to the stored value. Mismatch means installation aborts. This catches tampered packages and MITM attacks.
Watch for Lock File Poisoning
A malicious PR can modify the lockfile to point to a different package version or registry URL while the package.json diff looks clean. Reviewers often skip lockfile diffs because they're large and auto-generated.
TIP
Always review lockfile changes in PRs. Tools like Socket and Snyk flag suspicious lockfile modifications automatically.
5. GitHub Actions Security
GitHub Actions is the CI/CD layer where all your dependency management happens. A compromised workflow can steal every secret in your repository.
Pin Actions to Full Commit SHAs
Git tags are mutable. An attacker who compromises an action's repository can move a tag (v4) to point to malicious code. Every workflow referencing @v4 then executes the attacker's code. This is exactly what happened in the tj-actions/changed-files incident (March 2025, CVE-2025-30066).
yaml
# DANGEROUS — mutable tag
- uses: actions/checkout@v4
# SAFE — immutable commit SHA
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2The trailing comment preserves readability. The SHA is immutable — even if the repository is compromised, your workflow runs the exact code you audited.
Set up Dependabot to keep pinned SHAs current:
yaml
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"Restrict GITHUB_TOKEN Permissions
By default, GITHUB_TOKEN can have broad read/write access. A compromised step with an over-permissioned token can modify code, create releases, or exfiltrate data.
yaml
# Workflow-level: deny all by default
permissions: {}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read # only what this job needs
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false # don't leave token in git configAlways set permissions: {} at the workflow level, then grant per-job.
Prevent Workflow Injection
GitHub Actions expressions ($) are interpolated before the YAML is parsed. Untrusted input in a run: block becomes arbitrary code execution.
yaml
# DANGEROUS — attacker controls PR title
- run: echo "PR: ${{ github.event.pull_request.title }}"
# SAFE — environment variable is data, not code
- env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: echo "PR: $PR_TITLE"Attacker-controlled fields to never interpolate directly:
github.event.pull_request.title/.bodygithub.event.issue.title/.bodygithub.event.comment.bodygithub.event.head_commit.messagegithub.head_ref(branch name)
Use OIDC Instead of Long-Lived Secrets
Instead of storing cloud provider credentials as repository secrets, use OpenID Connect for short-lived tokens:
yaml
jobs:
deploy:
permissions:
id-token: write
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
aws-region: us-east-1
# No access key — uses OIDC token exchangeyaml
jobs:
deploy:
permissions:
id-token: write
contents: read
steps:
- uses: google-github-actions/auth@6fc4af4b145ae7821d527454aa9bd537d1f2dc5f # v2.1.7
with:
workload_identity_provider: 'projects/ID/locations/global/workloadIdentityPools/POOL/providers/PROVIDER'
service_account: 'sa@project.iam.gserviceaccount.com'yaml
jobs:
deploy:
permissions:
id-token: write
contents: read
steps:
- uses: azure/login@a65d910e8af852a8061c627c456678983e180302 # v2.2.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
# Uses federated credentials, no client secretOIDC tokens are scoped to a specific workflow run and expire in minutes. No secrets to rotate or leak.
Add Dependency Review to PRs
yaml
name: Dependency Review
on: pull_request
permissions:
contents: read
pull-requests: write
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.5.0
with:
fail-on-severity: moderate
deny-licenses: GPL-3.0, AGPL-3.0
comment-summary-in-pr: alwaysThis compares the dependency graph of the PR branch against the base branch and flags newly introduced CVEs or problematic licenses.
Self-Hosted Runner Rules
DANGER
Never use self-hosted runners with public repositories unless they are ephemeral and disposable. Any user who can open a pull request can execute code on your runner — accessing your network, installing persistent malware, or stealing credentials from previous jobs.
If you must use self-hosted runners:
- Use
--ephemeralmode — the runner picks up one job and de-registers - Use runner groups to restrict which repos can target which runners
- Put runners in an isolated network segment
- Consider Actions Runner Controller (ARC) for Kubernetes-based ephemeral pods
6. Dependency Scanning Tools
No single tool catches everything. Layer them.
| Tool | Approach | Catches | Free? |
|---|---|---|---|
| npm audit | CVE database lookup | Known vulnerabilities | Yes |
| pip-audit | CVE database (OSV) | Known vulnerabilities | Yes |
| GitHub Dependabot | CVE alerts + auto-update PRs | Known vulns, outdated deps | Yes |
| Snyk | CVE database + proprietary research | Known vulns (larger DB) | Free tier |
| Socket | Behavioral code analysis | Malicious code, typosquats, install scripts | Free for OSS |
Socket — Why It's Different
Traditional scanners (npm audit, Snyk, Dependabot) check packages against databases of known, reported CVEs. Socket takes a fundamentally different approach — it performs behavioral analysis on package source code to detect intentionally malicious patterns even when no CVE exists yet.
Socket monitors 70+ behavioral signals including:
- Install scripts that execute shell commands
- Network requests to unknown endpoints
- Environment variable access and exfiltration
- Obfuscated or dynamically evaluated code
- Typosquatting via name similarity analysis
- Maintainer account changes and suspicious ownership transfers
CLI usage:
bash
# Install
npm install -g @socketsecurity/cli
# Safe npm wrapper — scans before installing
socket npm install <package>
# Safe npx wrapper
socket npx <package>
# Look up risks for a specific package
socket info <package>Socket Firewall Free — a standalone binary that blocks confirmed malware at any dependency depth, no account required:
bash
npm install -g sfw
# Wraps your package manager
sfw npm install <package>
sfw pip install <package>GitHub App — automatically analyzes dependency changes in PRs, posts comments highlighting security concerns, and supports branch protection rules to block merges.
TIP
The recommended layered approach: Dependabot for keeping dependencies current, Snyk for known vulnerability management, Socket for supply chain attack detection. These tools are complementary, not competitive.
7. Hardened CI Workflow Template
A complete example combining all the practices above:
yaml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions: {}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm test
dependency-review:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.5.0
with:
fail-on-severity: moderate
deploy:
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
with:
role-to-assume: arn:aws:iam::123456789012:role/DeployRole
aws-region: us-east-1
- env:
COMMIT_MSG: ${{ github.event.head_commit.message }}
run: ./deploy.sh8. Attack Vectors Quick Reference
| Vector | How It Works | Primary Defense |
|---|---|---|
| Typosquatting | Packages named lodsah, creat-react-app | Socket detection, careful install commands |
| Dependency confusion | Public package with same name as internal, higher version | Registry pinning, scoped packages in .npmrc |
| Install scripts | postinstall hooks execute arbitrary code | ignore-scripts=true in .npmrc |
| Account takeover | Phishing/credential stuffing gives access to maintainer account | 2FA, provenance verification, Trusted Publishing |
| Protestware | Maintainer deliberately sabotages own package | Version pinning, lockfiles |
| Expired domain hijack | Attacker registers maintainer's expired email domain, resets password | Monitor domain registration |
| Lockfile poisoning | PR modifies lockfile to point to malicious version | Review lockfile diffs, use Socket/Snyk |
| Mutable action tags | Attacker moves v4 tag to malicious commit | Pin actions to full commit SHA |
| Workflow injection | Untrusted input interpolated in run: blocks | Use intermediate environment variables |
| CI secret exfiltration | Compromised action dumps secrets to logs | Least-privilege GITHUB_TOKEN, pin SHAs |
| New package / version | Malicious code published under fresh name or hijacked release | 7-day quarantine policy, Socket alerts |
9. Defensive Commands Cheatsheet
npm
bash
npm ci # Strict lockfile install for CI
npm audit # Check for known vulnerabilities
npm audit signatures # Verify package provenance
npm audit fix # Auto-fix semver-compatible vulns
npm install --ignore-scripts # Skip all lifecycle scripts
npm publish --provenance # Publish with Sigstore provenance
npm config set ignore-scripts true # Persist ignore-scripts globally
npm token create --read-only # Create restricted CI token
npm profile enable-2fa auth-and-writes # Enable full 2FA
npm view <pkg> time --json # Check publish dates (7-day rule)pip
bash
pip-audit # Scan for known vulnerabilities
pip-audit --fix # Auto-fix vulnerable packages
pip-audit -r requirements.txt # Audit a requirements file
pip install --require-hashes -r requirements.txt # Hash-verified install
pip install --isolated # Ignore all config files
pip config set global.require-virtualenv true # Force venv usage
pip-compile --generate-hashes # Generate hashed requirementsGitHub Actions
yaml
permissions: {} # Deny all at workflow level
persist-credentials: false # Don't leave tokens in git config
uses: action@<full-sha> # vX.Y.Z # Pin to immutable commit
id-token: write # Enable OIDC (per-job only)Related
- GitHub Setup — SSH keys, credential management, and pre-commit hooks that complement supply chain defenses
- Git Branch Hygiene — PR discipline and branch workflows that reduce the surface area for lockfile poisoning
- Docker Quick Reference — Container image security shares many supply chain principles with package security