How to Secure Your npm Supply Chain in 2026
·PkgPulse Team
TL;DR
npm supply chain attacks tripled 2022–2025 — and most teams have zero protection beyond npm audit. The real threats aren't in your direct dependencies; they're in the 500+ transitive ones. Fix: pin lockfiles, automate npm audit in CI, enable npm provenance for your packages, scan with Socket.dev, and configure package.json overrides for vulnerable transitive deps. Takes an afternoon to set up, prevents months of incident response.
Key Takeaways
- Lock everything —
package-lock.jsonorpnpm-lock.yamlmust be committed and checked in CI npm audit --audit-level=high— break CI on high/critical vulns, ignore noisy low-severity- Socket.dev — detects malicious packages before you install them (behavioral analysis, not just CVE matching)
- Provenance attestation — verify packages were built from their claimed source repo
- Overrides/resolutions — force-patch vulnerable transitive dependencies without waiting for upstream
The 5 Attack Vectors Targeting npm in 2026
1. Typosquatting
lodahs, momentjs, reacts — malicious packages named like popular ones
Defense: exact name matching, audit new installs
2. Dependency confusion
Internal package names published to public npm to intercept installs
Defense: scoped packages (@yourcompany/), private registry with priority
3. Account takeover
Attacker gains npm token of a maintainer, publishes malicious patch version
Defense: 2FA enforcement, provenance attestation, Socket.dev monitoring
4. Protestware / Intentional sabotage
Maintainer deliberately adds malicious code (node-ipc, colors incidents)
Defense: lockfiles, Socket.dev behavioral analysis, pin major versions
5. Transitive dependency injection
Compromise an unused utility package that happens to be in your dep tree
Defense: dependency graph auditing, minimize dep count
Most attacks in 2025 targeted packages with:
- Single maintainer (no review process)
- Dormant maintainer account
- High download count but low scrutiny
Defense Layer 1: Lockfiles
# Your lockfile IS your supply chain snapshot
# Always commit it — it pins exact versions including transitive deps
# npm: package-lock.json
npm install # Creates/updates lockfile
npm ci # CI: install ONLY from lockfile, no resolution
# pnpm: pnpm-lock.yaml
pnpm install # Creates/updates
pnpm install --frozen-lockfile # CI: fail if lockfile would change
# yarn: yarn.lock
yarn install
yarn install --frozen-lockfile # CI equivalent
# The critical difference:
# npm install → can update transitive deps, lockfile may drift
# npm ci → installs exactly what's in lockfile, fails if package.json ≠ lockfile
# .github/workflows/ci.yml — enforce lockfile in CI
- name: Install dependencies
run: npm ci # NOT npm install
# npm ci: fails fast if lockfile is out of sync with package.json
# Prevents "it works locally" supply chain drift
# Verify your lockfile hasn't been tampered with
# Check for unexpected changes in PR reviews:
git diff HEAD~1 -- package-lock.json | grep '"resolved"' | head -20
# Look for unexpected new URLs or checksums
# Alert: packages being fetched from non-registry URLs
# 🚨 "resolved": "https://github.com/..." instead of "https://registry.npmjs.org/..."
Defense Layer 2: npm Audit Automation
# Basic audit
npm audit
# Break on high and critical only (recommended for CI)
npm audit --audit-level=high
# JSON output for custom processing
npm audit --json | jq '.vulnerabilities | keys[]'
# Fix automatically (patches + minor only — review major upgrades manually)
npm audit fix
# Force fix (includes major bumps — test thoroughly)
npm audit fix --force
# CI: fail on high/critical vulnerabilities
# .github/workflows/security.yml
name: Security Audit
on:
push:
branches: [main]
pull_request:
schedule:
- cron: '0 8 * * 1' # Weekly Monday 8am
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- name: Security audit
run: npm audit --audit-level=high
# Exits with code 1 if high/critical vulns found
# Low/moderate: informational only, don't block CI
- name: Check for known malicious packages
uses: step-security/harden-runner@v2
with:
egress-policy: audit # Log all outbound network calls
# Audit noise reduction — suppress known acceptable risks
# .npmrc
audit-level=high # Suppress low/moderate in npm audit output
# Or use .auditignore equivalent via overrides (see Layer 4)
Defense Layer 3: Socket.dev (Proactive Scanning)
# Socket catches what npm audit misses:
# npm audit: "is this package in the CVE database?"
# Socket: "is this package DOING SUSPICIOUS THINGS?"
# Socket analysis flags:
# - New install scripts (postinstall that wasn't there before)
# - New network access code
# - New filesystem access
# - Obfuscated code
# - Newly published by someone other than the usual maintainer
# - Package that just changed maintainers
# Install Socket CLI
npm install -g @socket/cli
# Scan before installing new packages
socket npm install lodash # Scans lodash + deps before install
socket npm install react react-dom # Scans the whole tree
# One-time project scan
socket scan create --view
# GitHub Actions: Socket security PR checks
# Adds a comment to PRs when package.json changes introduce risky packages
# Install: github.com/SocketDev/socket-security-github-action
name: Socket Security
on:
pull_request:
paths:
- 'package.json'
- 'package-lock.json'
- 'pnpm-lock.yaml'
jobs:
socket:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: SocketDev/socket-security-github-action@v1
with:
api-key: ${{ secrets.SOCKET_SECURITY_API_KEY }}
Defense Layer 4: Patch Transitive Vulnerabilities
# Problem: vulnerable transitive dep that your direct dep hasn't fixed
# Solution: force override the version
# npm overrides (npm 8.3+):
# package.json
{
"overrides": {
"semver": ">=7.5.2", # CVE-2022-25883 — ReDoS in older semver
"glob-parent": ">=5.1.2", # Vulnerability in transitive dep
"minimatch": ">=3.0.5" # ReDoS fix
}
}
# pnpm overrides (same concept):
{
"pnpm": {
"overrides": {
"semver@<7.5.2": ">=7.5.2",
"ip@<2.0.1": ">=2.0.1"
}
}
}
# yarn resolutions:
{
"resolutions": {
"semver": ">=7.5.2"
}
}
# After adding overrides:
npm install # Applies overrides
npm audit # Verify the vuln is resolved
# Check what version was actually installed
npm ls semver # Shows version tree
Defense Layer 5: npm Provenance Attestation
# Provenance: cryptographic proof that a package was built
# from a specific GitHub repo at a specific commit
# Introduced 2023 — now critical for high-trust packages
# Verify provenance when installing (npm 9.5+):
npm install react --foreground-scripts
# Look for: "Attestation verified: sigstore"
# Check package provenance manually:
npm view react dist.attestations
# Shows: {type: "sigstore/provenance", ...}
# Provenance tells you:
# - Exact GitHub repo it was built from
# - Exact commit hash
# - CI workflow that built it
# - Build timestamp
# Red flag: package claims to be from a popular org
# but has no provenance (especially for recently-published packages)
# Publishing with provenance (for package authors):
# .github/workflows/publish.yml
name: Publish to npm
on:
push:
tags: ['v*']
permissions:
contents: read
id-token: write # Required for OIDC provenance
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- run: npm ci
- run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# --provenance: attaches sigstore attestation to the publish
# Proves this package was built from THIS repo at THIS commit
Defense Layer 6: Dependency Minimization
# The safest dependency is the one you don't have
# Each dependency is an attack surface
# Find unused dependencies
npx depcheck
# Reports: Unused dependencies, Missing dependencies
# Analyze what's actually big
npx cost-of-modules # Shows size contribution of each dep
npx bundlephobia-cli react react-dom # Bundle impact
# Check for built-in alternatives
# Before installing, ask:
# - Does Node.js have this built-in?
# fetch → built-in Node 18+
# crypto → built-in
# fs/path → built-in
# uuid → crypto.randomUUID() built-in
# Prefer packages with:
# - Zero dependencies (or very few)
# - Single maintainer with provenance
# - High download velocity (more eyes on the code)
# - TypeScript-native (safer, typed API)
# Example: replace moment (deps: 0, but 300KB)
# with date-fns (functional, tree-shakeable) or
# Temporal API polyfill (upcoming standard)
Defense Layer 7: Private Registry + Scoped Packages
# Dependency confusion attack:
# If you have internal packages named "utils", "helpers", "api-client"
# An attacker publishes "utils" to public npm with a higher version
# npm resolves public registry first → you install malware
# Fix 1: Use scoped packages for everything internal
# Internal: "@mycompany/utils" instead of "utils"
# npm won't find "@mycompany/utils" on public registry unless you published it
# Fix 2: Private registry with priority
# .npmrc — use private registry for @mycompany scope
@mycompany:registry=https://npm.mycompany.com/
# Public packages still come from registry.npmjs.org
# Fix 3: Block all unscoped private names from public registry
# .npmrc
@mycompany:registry=https://npm.mycompany.com/
always-auth=true
# Verdaccio: self-hosted private npm registry
# docker run -it --rm --name verdaccio -p 4873:4873 verdaccio/verdaccio
Security Checklist for 2026
Pre-install:
[ ] Search for typos in package name (lodash not lodahs)
[ ] Check package age and download count
[ ] Verify maintainer reputation (recently published first version = risk)
[ ] Run: socket npm install <package> (pre-install scan)
Project setup:
[ ] Commit lockfile (package-lock.json / pnpm-lock.yaml)
[ ] Use npm ci in CI (not npm install)
[ ] Use scoped names for internal packages
[ ] Set up private registry if you have internal packages
CI pipeline:
[ ] npm audit --audit-level=high (fail on high/critical)
[ ] Socket GitHub Action (flag risky package.json changes)
[ ] Weekly scheduled audit run
[ ] Dependency review on PRs (github.com/actions/dependency-review-action)
Ongoing:
[ ] Dependabot or Renovate for automated security updates
[ ] Monitor npm security advisories for packages you use
[ ] Review package.json changes in PRs carefully
[ ] Check for unexpected postinstall scripts
If you publish packages:
[ ] Publish with --provenance
[ ] Use 2FA on npm account
[ ] Set up npm Automation tokens (not user tokens) for CI
[ ] Review package contents before publishing: npx npm-publish-dry-run
Tools Summary
| Tool | What It Catches | When to Run |
|---|---|---|
npm audit | Known CVEs in dep tree | CI on every push |
| Socket.dev | Malicious behavior patterns | Pre-install, PR review |
| Dependabot | Outdated deps with patches | Continuous (PR per fix) |
npm ci | Lockfile drift | CI install step |
depcheck | Unused dependencies | Monthly cleanup |
snyk | CVEs + license issues | CI alternative to npm audit |
Check health scores and security data for any npm package at PkgPulse.
See the live comparison
View npm vs. pnpm on PkgPulse →