How to protect your project from npm supply chain attacks

· Tech

Why this matters

In 2025, Sonatype identified over 454,600 new malicious open-source packages across major ecosystems, and over 99% of them were on npm. The biggest attacks hit packages like chalk, debug, and nx through stolen credentials and phishing. In March 2026, even axios (100M weekly downloads) was hijacked.

Many of these attacks relied on malicious code that ran during npm install through lifecycle scripts (preinstall/postinstall), though some (like the chalk/debug compromise) injected runtime code instead. Most were detected within hours, but that was enough to cause damage.

This is my note on what to do about it.

Use pnpm

pnpm v10 has unusually strong supply-chain controls. Three settings in pnpm-workspace.yaml would have reduced exposure to many high-profile attacks from 2025-2026:

pnpm-workspace.yaml
# Only install packages older than 7 days.
# Most malicious packages are caught and removed within hours.
minimumReleaseAge: 10080
# Block trust downgrades.
# If a package was published through verified CI/CD before,
# block versions published with a regular token.
trustPolicy: no-downgrade
# Block all lifecycle scripts by default.
# Whitelist only the packages that need them.
# (pnpm 10.26.0+, replaces deprecated onlyBuiltDependencies)
strictDepBuilds: true
allowBuilds:

You can exclude your own packages from the age check:

minimumReleaseAgeExclude:
- "@myorg/*"

References:

Pin exact versions

Add this to .npmrc:

save-exact=true
save-prefix=''

In pnpm v10, dependency lifecycle scripts are already blocked unless explicitly allowed via allowBuilds, so ignore-scripts=true is not needed. If you use npm, add ignore-scripts=true here as well.

This prevents surprise updates from semver ranges. Use Renovate or Dependabot for controlled, reviewable dependency updates.

Use frozen lockfiles in CI

Always use frozen lockfiles in CI so your pipeline installs exactly what is in your lockfile:

Terminal window
pnpm install --frozen-lockfile

Even if a malicious version is published, your CI will not pull it unless someone explicitly updates the lockfile.

Install Socket Firewall

Socket Firewall is a free tool that blocks known malicious packages at install time. It works as a proxy between your package manager and the registry.

Terminal window
npm i -g sfw

Make it permanent with a shell alias:

Terminal window
# Add to .zshrc or .bashrc
alias pnpm='sfw pnpm'

If you use GitHub, add the Socket for GitHub app. It monitors dependency changes in PRs and analyzes new dependency risk.

References:

Use Docker for installs

Running pnpm install inside Docker limits what a malicious package can access. By default, it cannot reach your SSH keys, cloud credentials, or home directory, but it can still read build context, environment variables, .npmrc tokens, and exfiltrate data over the network.

# Stage 1: install deps in isolation
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
# Stage 2: build with source code
FROM node:22-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm build

Docker is a containment layer, not a prevention layer. Use it alongside the tools above, not instead of them.

Reduce your attack surface

A 2019 npm ecosystem study found the average package depends on 79 transitive packages from 39 maintainers. Every dependency is a potential risk.

Before adding a new package:

The es-tooling/module-replacements repo lists npm packages that can be replaced with native alternatives.

If you publish packages

If your project publishes npm packages, you are also a potential target. Both the Nx and Axios compromises happened because attackers got access to publish tokens.

Other tools

Quick reference

pnpm-workspace.yaml
minimumReleaseAge: 10080
trustPolicy: no-downgrade
strictDepBuilds: true
allowBuilds:
.npmrc
save-exact=true
save-prefix=''
Terminal window
# Install Socket Firewall
npm i -g sfw
# Use it
sfw pnpm install --frozen-lockfile

Further reading