Skip to main content

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 everythingpackage-lock.json or pnpm-lock.yaml must 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

ToolWhat It CatchesWhen to Run
npm auditKnown CVEs in dep treeCI on every push
Socket.devMalicious behavior patternsPre-install, PR review
DependabotOutdated deps with patchesContinuous (PR per fix)
npm ciLockfile driftCI install step
depcheckUnused dependenciesMonthly cleanup
snykCVEs + license issuesCI 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 →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.