IAnswerable

Protect Your Build from Supply Chain Attacks

13 May 2026

The attack that hits small teams isn’t a nation-state exploit in your own code. It’s a maintainer’s npm account getting phished, and a malicious version of a package you already trust getting published and auto-installed on your next npm install. Here’s what actually reduces that risk, with almost no ongoing effort.

Commit your lockfile, and mean it

If you don’t have package-lock.json (or yarn.lock, pnpm-lock.yaml) committed and enforced, every install can silently resolve to a different version of every transitive dependency depending on what’s newest that day — including whatever a compromised maintainer pushed an hour ago.

npm ci   # not npm install, in CI and on servers

npm ci installs exactly what’s in the lockfile and fails if package.json and the lockfile disagree, instead of quietly resolving a new tree. This one flag switch is the highest-leverage change on this list and costs nothing.

Pin GitHub Actions to a commit SHA, not a tag

# don't
- uses: actions/checkout@v4

# do
- uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v4.1.1

A tag like v4 can be moved to point at a different commit. A SHA can’t. This matters specifically for third-party actions with less scrutiny than the official actions/* ones — a compromised action with secrets access in your workflow can exfiltrate your CI secrets directly. Dependabot can keep SHA-pinned actions updated for you, so you don’t lose the convenience of v4, just the ability for it to silently point somewhere new.

Don’t run postinstall scripts you haven’t reviewed, if you can avoid it

postinstall hooks are how a growing number of real supply chain attacks execute — malicious code runs the moment you install, before your app even starts. You can’t disable this globally without breaking legitimate packages that need it (native module builds, mostly), but you can audit what’s actually running:

npm ls --all --parseable | xargs -I{} sh -c 'test -f {}/package.json && jq -r "select(.scripts.postinstall) | .scripts.postinstall" {}/package.json 2>/dev/null'

For CI environments specifically, where you’re not going to manually review this, npm ci --ignore-scripts is a reasonable default if none of your actual dependencies need native builds — check first, since this breaks packages like sharp or bcrypt that compile on install.

Provenance: verify where a package actually came from

npm now supports publish provenance — a cryptographically signed, publicly verifiable link between a package version and the exact CI run and source commit that built it:

npm audit signatures

This checks installed packages against npm’s registry signatures. Not every package publishes with provenance yet, but the ones that do (increasingly the popular ones, post several high-profile incidents) give you a real answer to “did this actually come from the maintainer’s repo,” not just “does the version number look right.”

Reduce your surface area, honestly

Every dependency is a package whose maintainer’s account security you’re now trusting. Before adding one for something you could write in 20 lines, don’t. This is the least technical mitigation on this list and the most effective one — an attack surface you don’t have doesn’t need defending.

What to actually do this week

Action Effort Impact
Switch CI/deploy to npm ci 5 minutes High
Pin GitHub Actions to SHAs 30 minutes High
Run npm audit signatures 1 minute Medium
Full postinstall script audit An afternoon Medium

The concrete next action

Check whether your CI pipeline runs npm install or npm ci. If it’s install, that’s the single highest-impact fix on this page, and it’s a one-word change.