TL;DR
NPM supply chain attacks surged to over 3,000 malicious packages in 2024 alone, and the March 2026 Axios compromise proved even top-10 packages aren’t safe. This guide shows API developers how to harden npm usage with lockfile enforcement, postinstall script blocking, exact version pinning, provenance checks, behavioral analysis, dependency reduction, and CI/CD monitoring.
Introduction
The Axios supply chain attack on March 31, 2026, wasn’t the first npm compromise. It won’t be the last. But with 83 million weekly downloads and a cross-platform RAT deployed through a single hijacked maintainer account, it was one of the clearest warnings the JavaScript ecosystem has received.
The problem is that the attack bypassed common “just update your dependencies” advice. The malicious code wasn’t in Axios itself. It was injected through a phantom dependency that triggered a postinstall hook. Lockfiles didn’t help if you ran npm install during the attack window. Version pinning didn’t help if you had not pinned yet.
API developers are especially exposed. Test scripts, CI/CD jobs, mock servers, and HTTP clients often pull from npm. A compromised package in that toolchain can leak API keys, database credentials, cloud tokens, and local developer secrets.
Apidog reduces one source of npm exposure by providing a built-in HTTP client for API testing, so you do not need Axios, node-fetch, or got in your API testing stack. You can try Apidog free while applying the defense layers below.
This guide walks through seven practical layers you can add to your npm workflow.
Layer 1: Enforce lockfiles
Why lockfiles matter
A lockfile records the exact version of every direct and transitive dependency installed at a given time.
Without a lockfile, npm install resolves the latest version that matches your semver range. For example:
{
"dependencies": {
"axios": "^1.14.0"
}
}
The ^1.14.0 range allows npm to install later compatible 1.x.x versions. If a malicious 1.14.1 exists on the registry during install, npm can resolve to it.
What to do
Commit your lockfile:
-
package-lock.jsonfor npm -
yarn.lockfor Yarn -
pnpm-lock.yamlfor pnpm -
bun.lockfor Bun
Use frozen installs in CI/CD instead of regular installs:
# npm
npm ci
# yarn
yarn install --frozen-lockfile
# pnpm
pnpm install --frozen-lockfile
# bun
bun install --frozen-lockfile
npm ci deletes node_modules and installs strictly from package-lock.json. If package.json and the lockfile do not match, the command fails.
Add lockfile review to PRs
When a PR changes a lockfile, review it like application code.
Check for:
- New transitive dependencies
- Unexpected version bumps
- Registry URL changes
- New packages with install scripts
- Packages with similar names to popular libraries
Behavioral analysis tools such as Socket.dev can also flag suspicious lockfile changes in pull requests.
The gap
Lockfiles protect against unexpected version resolution after the dependency has been locked. They do not protect the first install.
If you add a dependency during an attack window, the malicious package can still be written into your lockfile. That is why lockfiles are the foundation, not the full solution.
Layer 2: Disable postinstall scripts
Why this matters
Many npm supply chain attacks use lifecycle scripts:
preinstallinstallpostinstallprepare
These scripts run during dependency installation, before your app starts and before runtime monitoring can help.
The Axios attack, the ua-parser-js attack, the event-stream attack, and many others used this pattern.
Block scripts globally
Add this to your project .npmrc:
ignore-scripts=true
Or configure it globally:
npm config set ignore-scripts true
This prevents npm lifecycle scripts from running during package installation.
Handle packages that require scripts
Some packages need lifecycle scripts for native compilation or binary setup, such as bcrypt, sharp, or sqlite3.
Use one of these patterns.
Option 1: Install without scripts, then rebuild only trusted packages
npm ci --ignore-scripts
npm rebuild bcrypt sharp
Option 2: Use an allowlist where supported
For npm 10+, create a .scriptsrc.json file and allow only trusted packages:
{
"allowScripts": {
"bcrypt": true,
"sharp": true
}
}
Option 3: Prefer prebuilt binaries
Some native packages now ship prebuilt binaries for common platforms. For example, sharp provides prebuilt binaries for many environments, which can reduce the need for postinstall compilation.
Before allowing scripts, check whether the package still needs them in your target runtime.
Watch Git-based dependencies
In January 2026, researchers disclosed six zero-day vulnerabilities called “PackageGate” affecting npm, pnpm, vlt, and Bun.
One finding showed that Git-based dependencies can carry configuration files that enable code execution even when lifecycle scripts are disabled.
If your package.json uses Git URLs, do not rely only on ignore-scripts.
Pin Git dependencies to specific commit hashes:
{
"dependencies": {
"some-package": "github:org/repo#8f4d3c2a1b..."
}
}
Also review the repository contents before adding it.
Layer 3: Pin exact versions
Stop using semver ranges for critical dependencies
By default, npm saves dependencies with a caret:
{
"dependencies": {
"axios": "^1.14.0"
}
}
The caret means npm can resolve to later compatible versions. During an active compromise, that can be dangerous.
Use exact versions instead:
{
"dependencies": {
"axios": "1.14.0"
}
}
Configure npm to save exact versions by default:
# .npmrc
save-exact=true
save-prefix=
Pin transitive dependencies with overrides
Direct dependencies have their own dependencies. If a transitive dependency is compromised, pinning only your direct dependencies is not enough.
For npm, use overrides:
{
"overrides": {
"axios": "1.14.0",
"plain-crypto-js": "npm:empty-npm-package@1.0.0"
}
}
For Yarn, use resolutions:
{
"resolutions": {
"axios": "1.14.0"
}
}
For pnpm, use pnpm.overrides:
{
"pnpm": {
"overrides": {
"axios": "1.14.0"
}
}
}
Trade-off
Exact pinning means you do not receive patch updates automatically. You must intentionally bump dependency versions.
For security-sensitive projects, this is usually the right trade-off: controlled updates are safer than automatic dependency drift.
Layer 4: Verify package provenance
What provenance proves
npm provenance attestation links a published package to its source code and build environment using Sigstore signatures in a public transparency ledger.
A package with valid provenance can prove:
- Which source repository it was built from
- Which CI/CD system built it
- Which commit triggered the build
Check provenance locally or in CI
Run:
npm audit signatures
This verifies that installed packages with attestations have valid signatures.
Packages published manually from a developer machine may not have provenance. For high-download or security-sensitive packages, lack of provenance should increase review scrutiny.
The malicious Axios versions lacked OIDC provenance binding and had no corresponding GitHub commits. Standard provenance checks would have made that suspicious.
Enable provenance for your own npm packages
If you publish packages, enable provenance in CI/CD.
Example GitHub Actions publish step:
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://registry.npmjs.org
- run: npm publish --provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
You can also add this to .npmrc:
provenance=true
Limitations
Provenance is not a guarantee that code is safe.
It proves where and how a package was built. It does not prove that the source code is benign. A compromised CI/CD pipeline could still publish a malicious package with valid provenance.
Use provenance as one signal in a layered security pipeline.
Layer 5: Use behavioral analysis tools
Why vulnerability scanning is not enough
Tools like npm audit and Snyk check known vulnerability databases. They are useful, but they miss zero-day supply chain attacks before disclosure.
Behavioral tools inspect what packages do, including:
- Network calls during install
- File system access outside the package directory
- Shell command execution
- Environment variable access
- Obfuscated code
- Credential harvesting patterns
Socket.dev
Socket analyzes package behavior during installation and runtime.
It can flag:
- Network requests during install
- File writes outside expected directories
- Shell commands
- Suspicious access to environment variables
- Obfuscated JavaScript
Install and scan:
npm install -g @socketsecurity/cli
socket scan
When integrated with GitHub, Socket can comment on pull requests when new dependencies introduce suspicious behavior.
In the Axios attack, the plain-crypto-js dependency would have triggered multiple suspicious signals, including obfuscated code, network access during postinstall, and file system writes outside the package directory.
Snyk
Snyk is stronger for known vulnerabilities, exploit maturity, risk scoring, and remediation guidance.
Install and test:
npm install -g snyk
snyk test
Use a layered scan
Run all three checks in CI:
# Baseline vulnerability audit
npm audit
# Behavioral analysis
socket scan
# Vulnerability management
snyk test
Treat critical findings as build blockers.
Layer 6: Minimize your dependency surface
Every dependency is a trust decision
The most effective way to reduce npm supply chain risk is to have fewer packages to trust.
The average Node.js project can contain hundreds of transitive dependencies. Each dependency can execute code, publish updates, change maintainers, or pull in additional packages.
Audit your dependency tree
Count installed dependencies:
npm ls --all | wc -l
Find duplicates and heavily repeated packages:
npm ls --all | sort | uniq -c | sort -rn | head -20
For each dependency, ask:
- Does Node.js provide this natively?
- Does this package pull in many transitive dependencies?
- Is it still maintained?
- Is it needed in production or only development?
- Can a small utility be vendored directly into the codebase?
Replace packages with native APIs where possible
| Package | Native alternative | Available since |
|---|---|---|
axios, node-fetch, got
|
fetch global |
Node.js 18 |
uuid |
crypto.randomUUID() |
Node.js 19 |
dotenv |
--env-file flag |
Node.js 20.6 |
chalk |
util.styleText() |
Node.js 21.7 |
glob |
fs.glob() |
Node.js 22 |
path-to-regexp |
Native URL pattern API | Node.js 23 |
Example: replace uuid with Node.js native crypto:
import { randomUUID } from "node:crypto";
const requestId = randomUUID();
Example: replace node-fetch in Node.js 18+:
const response = await fetch("https://api.example.com/users");
const data = await response.json();
Reduce API testing dependencies
API testing workflows often include:
- HTTP client libraries
- Assertion libraries
- Test runners
- Mock servers
- Documentation generators
Each adds dependencies and transitive dependencies.
Apidog consolidates API testing into one platform:
- Built-in HTTP client
- Visual test builder with assertions
- Automated test scenarios
- CI/CD integration through Apidog CLI
- Smart mock server with dynamic responses
- Documentation generated from API specs
Moving API testing into Apidog can reduce npm dependencies in your testing infrastructure, which means fewer packages to review and fewer install-time risks.
You can try Apidog free to consolidate your API testing stack.
Layer 7: Monitor network and runtime behavior
Block known-bad domains
After a supply chain attack, block known command-and-control infrastructure at the network level.
Example local block:
echo "0.0.0.0 sfrclak.com" | sudo tee -a /etc/hosts
For CI/CD, use a stricter model: allow only the domains your build needs.
Typical allowed destinations:
- npm registry
- Git provider
- Container registry
- Cloud deployment target
- Internal artifact repository
Everything else should be denied or at least audited.
Use StepSecurity Harden-Runner for GitHub Actions
StepSecurity Harden-Runner monitors GitHub Actions workflows in real time.
It provides:
- Outbound network monitoring
- Process execution tracking
- File integrity monitoring
- Alerts for anomalous behavior
Example:
- uses: step-security/harden-runner@v2
with:
egress-policy: audit
For stricter builds, switch from audit mode to block mode after you have reviewed expected network calls.
- uses: step-security/harden-runner@v2
with:
egress-policy: block
Monitor suspicious child processes
On developer machines and build agents, watch for unexpected processes spawned by node or npm.
Examples of suspicious child processes during install:
-
osascripton macOS -
cscripton Windows -
python3on Linux - Shell commands that read environment variables
- Commands that access SSH keys or cloud credentials
Endpoint detection and response tools can help identify these patterns.
Recommended .npmrc
Add a hardened .npmrc to your repository:
# Pin exact versions
save-exact=true
save-prefix=
# Disable lifecycle scripts
ignore-scripts=true
# Enable provenance for publishing
provenance=true
# Use the official registry
registry=https://registry.npmjs.org/
# Require web-based auth flow
auth-type=web
# Audit threshold
audit-level=moderate
Commit this file so the whole team uses the same npm defaults.
CI/CD security pipeline example
Here is a GitHub Actions workflow that applies the layers in one pipeline:
name: Secure Build
on: [push, pull_request]
jobs:
security-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: step-security/harden-runner@v2
with:
egress-policy: audit
- uses: actions/setup-node@v4
with:
node-version: 22
# Layer 1 + 2: Frozen lockfile, no lifecycle scripts
- run: npm ci --ignore-scripts
# Layer 3: Capture resolved dependency tree
- run: npm ls --all > deps.txt
# Layer 4: Check package signatures/provenance
- run: npm audit signatures
# Layer 5: Behavioral analysis
- run: npx socket scan
# Layer 5: Known vulnerability scan
- run: npx snyk test
# Layer 1: Baseline npm audit
- run: npm audit --audit-level=moderate
# Rebuild only explicitly approved native dependencies
- run: npm rebuild sharp bcrypt
For production use, add secrets only after install and scanning steps whenever possible. That reduces the impact of a malicious install-time script.
What’s coming next for npm security
Mandatory provenance for popular packages
npm is discussing provenance requirements for packages above certain download thresholds. This would reduce the risk of manual token-based publishing for widely used packages.
Two-person release approval
High-download packages may eventually require a second maintainer to approve releases. That would make a single compromised maintainer account less damaging.
Runtime permission scoping
Deno already restricts access to the network, file system, and environment variables unless explicitly granted. Node.js is exploring similar permission models.
When these models mature, install scripts and runtime code could be limited by explicit permissions.
Package manager convergence
pnpm’s stricter dependency isolation limits what packages can access. As npm and other package managers adopt stricter behavior, some dependency confusion and undeclared dependency issues become harder to exploit.
FAQ
What is an npm supply chain attack?
An npm supply chain attack targets your dependency chain instead of your application directly.
Attackers may:
- Compromise maintainer accounts
- Publish malicious package versions
- Add malicious transitive dependencies
- Publish typosquat packages
- Abuse install scripts
When you install or update dependencies, malicious code can run on your machine or inside CI/CD.
Is npm audit enough?
No.
npm audit checks known vulnerability databases. It is useful for CVEs and disclosed issues, but it does not reliably catch zero-day supply chain attacks.
Use it with:
- Lockfile enforcement
ignore-scripts=true- Exact version pinning
- Provenance checks
- Behavioral analysis tools
- Network monitoring
Should I stop using npm?
No.
npm remains the largest JavaScript package ecosystem, and most packages are safe. The goal is not to avoid npm entirely. The goal is to reduce exposure and control how dependencies enter your project.
How does Apidog help reduce npm supply chain risk?
Apidog provides a built-in HTTP client, test runner, mock server, and documentation generator for API development.
That can reduce the need for npm packages such as Axios, node-fetch, Jest, Express-based mock servers, and other testing dependencies. Fewer dependencies means fewer attack vectors in your API development workflow.
What is npm package provenance?
Package provenance uses Sigstore to cryptographically link a published npm package to its source repository and CI/CD build environment.
It helps answer:
- Where was this package built?
- Which commit produced it?
- Which workflow published it?
Verify it with:
npm audit signatures
How many npm packages are malicious?
Snyk identified over 3,000 malicious npm packages in 2024. By Q4 2025, Sonatype blocked 120,612 malware attacks in a single quarter across npm, PyPI, and other registries.
Most malicious packages are low-download typosquats, but high-profile compromises like Axios show that popular packages are not immune.
What is PackageGate?
PackageGate is a set of six zero-day vulnerabilities disclosed in January 2026 affecting npm, pnpm, vlt, and Bun.
One finding showed that Git-based dependencies can carry configuration files that enable code execution even when lifecycle scripts are disabled.
If you use Git dependencies:
- Pin them to commit hashes
- Review repository contents
- Avoid broad branch references like
main - Do not rely only on
ignore-scripts
Key takeaways
- Commit lockfiles and use frozen installs such as
npm ci. - Disable lifecycle scripts with
ignore-scripts=true. - Pin exact versions with
save-exact=true. - Use overrides to control transitive dependency resolution.
- Verify package signatures with
npm audit signatures. - Combine
npm audit, Socket.dev, and Snyk in CI. - Replace unnecessary packages with Node.js native APIs.
- Reduce API testing dependencies with integrated tools like Apidog.
- Monitor CI/CD network egress with StepSecurity Harden-Runner.
Every dependency is a trust decision. Reduce the number of decisions, verify the ones that remain, and monitor what your build does at install time.



Top comments (0)