Skip to content

How to Protect Your npm Projects from Supply Chain Attacks After the axios Compromise

Problem

When I ran npm install last week, I got this unexpected behavior:

npm install output
$ npm install
added 127 packages, and audited 128 packages in 3s
12 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities

The install succeeded. No vulnerabilities found. But the axios package in my project was now version 1.14.1 - a version that didn’t exist when I first wrote the code. And that version had been compromised.

Compromise alert
axios 1.14.1: Contains malicious code that exfiltrates environment variables
Affected versions: 1.13.5, 1.14.0, 1.14.1
Safe versions: 1.13.6, 1.7.9

My package.json had "axios": "^1.7.0" - the caret allowed npm to automatically upgrade to the compromised version.

Environment

  • Node.js: 20.x
  • npm: 11.x
  • Package: axios (HTTP client)
  • CI/CD: GitHub Actions
  • Project: Production API service

What Happened?

I have a Node.js project that uses axios for HTTP requests. My package.json looked like this:

package.json (before)
{
"dependencies": {
"axios": "^1.7.0",
"express": "^4.21.0",
"lodash": "^4.17.21"
}
}

The caret (^) in front of each version means “install any compatible version.” For axios, that means anything from 1.7.0 up to but not including 2.0.0.

When a new axios version (1.14.1) was published, my next npm install pulled it automatically. I didn’t notice because:

  1. npm audit showed 0 vulnerabilities initially
  2. The package had the same API - no breaking changes
  3. I was rushing to deploy a feature

But axios 1.14.1 had hidden malicious code.

The Attack Pattern

Supply chain attacks work like this:

Supply chain attack flow
Popular Package (axios)
|
v
Maintainer account compromised OR package hijacked
|
v
Malicious version published (looks legitimate)
|
v
Developers run npm install with loose version constraints
|
v
Malicious code executes during install or runtime
|
v
Credentials stolen, backdoors installed, data exfiltrated

Attackers specifically target developers who:

  • Use loose version constraints (^ or ~)
  • Run npm install without reviewing diff outputs
  • Trust packages from the official registry without verification

Why This Happened

1. Loose Version Constraints

The caret (^) is dangerous for security:

Version constraint behavior
"^1.7.0" means:
- 1.7.0: OK
- 1.7.1: OK (patch)
- 1.8.0: OK (minor)
- 1.14.1: OK (minor)
- 2.0.0: NOT OK (major change)
Any minor version update is auto-installed,
including compromised versions!

I thought the caret was convenient - it gets me bug fixes and features automatically. But it also gets me malware automatically.

2. No Lockfile Verification

I used npm install in my CI/CD pipeline instead of npm ci:

ci.yml (before)
jobs:
build:
steps:
- name: Install dependencies
run: npm install # WRONG - allows version drift

npm install can modify package-lock.json and pull newer versions. npm ci strictly follows the lockfile.

3. No Pre-Install Security Check

I ran npm install blindly. The Reddit discussion highlighted this exact problem - developers need to “slow down on the installs and actually read what the AI is pulling in.”

The Solution: Four Layers of Defense

I implemented four security practices after this incident.

Layer 1: Pin Exact Versions

I removed all caret and tilde prefixes:

package.json (after)
{
"dependencies": {
"axios": "1.7.9",
"express": "4.21.0",
"lodash": "4.17.21"
},
"devDependencies": {
"jest": "29.7.0",
"typescript": "5.3.3"
}
}

Now npm install will only install exactly these versions. If axios publishes 1.14.2, my project won’t pick it up automatically.

Layer 2: Use npm ci in CI/CD

I switched from npm install to npm ci:

ci.yml (after)
jobs:
build:
steps:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci --prefer-offline --no-audit
- name: Run npm audit
run: npm audit --audit-level=high

The key differences:

npm install vs npm ci comparison
npm install:
- Can modify package-lock.json
- May pull newer versions than lockfile
- Updates dependencies to satisfy semver
- Fast for development
npm ci:
- Strictly follows package-lock.json
- Deletes node_modules before install
- Fails if lockfile doesn't match package.json
- Fast and deterministic for CI

npm ci guarantees that every CI run uses the exact same versions as my local development.

Layer 3: Pre-Install Security Script

I created a pre-install check script:

pre-install-check.sh
#!/bin/bash
echo "Running security checks..."
# Check for lockfile
if [ ! -f "package-lock.json" ]; then
echo "Error: package-lock.json not found"
exit 1
fi
# Verify lockfile matches package.json
npm ls --depth=0 > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "Error: Lockfile integrity mismatch"
exit 1
fi
# Run security audit
if npm audit --audit-level=moderate; then
echo "Audit passed"
else
echo "Audit failed - review vulnerabilities before proceeding"
exit 1
fi
echo "All security checks passed"

I run this before any deployment.

Layer 4: Security Gate in CI/CD

I added a security gate that blocks builds with critical vulnerabilities:

security.yml
name: Security Gate
on: [push, pull_request]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci --prefer-offline --no-audit
- name: npm audit
run: npm audit --audit-level=high
continue-on-error: false
- name: Check package integrity
run: |
npm cache verify
npm ls --depth=0
- name: Scan for malicious packages
run: npx socket scan --strict
continue-on-error: false

This pipeline fails if:

  • npm audit finds high-severity vulnerabilities
  • socket scan detects known malicious packages
  • Lockfile integrity is broken

The .npmrc Security Configuration

I also added a .npmrc file for extra protection:

.npmrc
# Enable strict SSL
strict-ssl=true
# Always verify package integrity
package-lock=true
# Prefer offline packages (reduces network exposure)
prefer-offline=true
# Never install packages without lockfile
lockfile-version=3

This ensures npm:

  • Only connects via HTTPS
  • Verifies package checksums
  • Uses cached packages when available

Trial and Error Process

I tried a few approaches before settling on this solution.

First Attempt: Just npm audit

First attempt
$ npm audit
found 0 vulnerabilities

But npm audit only checks known vulnerabilities in the registry database. It doesn’t detect:

  • Brand-new compromises (not yet reported)
  • Malicious code hidden in legitimate-looking packages
  • Packages that steal credentials at runtime

I needed more than just audit.

Second Attempt: npm audit fix

Second attempt
$ npm audit fix
up to date, audited 128 packages in 2s

But npm audit fix updates versions - which could pull compromised versions if the fix suggests a newer version that hasn’t been flagged yet.

Final Solution: Pinning + npm ci + Security Gates

The combination works because:

Defense layers
Layer 1: Exact versions pinned
--> No automatic updates to unknown versions
Layer 2: npm ci in CI
--> CI uses exact lockfile versions
Layer 3: Pre-install checks
--> Verify before any install
Layer 4: Security gates
--> Block deployment if compromised package detected

Each layer catches what the previous might miss.

Typo-Squatting Attacks

Attackers create packages with names similar to popular ones:

Typo-squatting examples
Real package: axios
Fake package: axios-pro, axios2, a-xios
Real package: express
Fake package: expressjs, expres
Mistype one letter or add common suffix, get malware

Always verify package names exactly.

Package Lifecycle Attack

Even trusted packages can become compromised when:

  • Maintainer account is hijacked
  • Maintainer becomes inactive and package is transferred
  • Malicious maintainer added to team

Check maintainer history before trusting:

Check package metadata
$ npm info axios
{
name: 'axios',
version: '1.7.9',
maintainers: [
{ name: 'axios-team', email: '...' }
],
...
}

Tools for Enhanced Protection

Two tools I now use:

  1. Socket.dev - Scans dependencies for known malicious patterns
  2. Aikido Security’s Safe Chain - Prevents malware during npm install

These tools catch compromises that npm audit misses.

Summary

In this post, I showed how to protect npm projects from supply chain attacks after the axios compromise. The key points are:

  1. Pin exact versions - Remove ^ and ~ from package.json
  2. Use npm ci - Strict lockfile adherence in CI/CD
  3. Run npm audit - But don’t rely on it alone
  4. Add security gates - Block builds with suspicious packages
  5. Slow down - Review what you’re installing before running it

The axios compromise taught me that convenience (^ versioning) creates vulnerability. Deterministic builds (npm ci + pinned versions) create safety.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments