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 installadded 127 packages, and audited 128 packages in 3s
12 packages are looking for funding run `npm fund` for details
found 0 vulnerabilitiesThe 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.
axios 1.14.1: Contains malicious code that exfiltrates environment variablesAffected versions: 1.13.5, 1.14.0, 1.14.1Safe versions: 1.13.6, 1.7.9My 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:
{ "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:
npm auditshowed 0 vulnerabilities initially- The package had the same API - no breaking changes
- 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:
Popular Package (axios) | vMaintainer account compromised OR package hijacked | vMalicious version published (looks legitimate) | vDevelopers run npm install with loose version constraints | vMalicious code executes during install or runtime | vCredentials stolen, backdoors installed, data exfiltratedAttackers specifically target developers who:
- Use loose version constraints (
^or~) - Run
npm installwithout reviewing diff outputs - Trust packages from the official registry without verification
Why This Happened
1. Loose Version Constraints
The caret (^) is dangerous for security:
"^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:
jobs: build: steps: - name: Install dependencies run: npm install # WRONG - allows version driftnpm 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:
{ "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:
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=highThe key differences:
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 CInpm 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:
#!/bin/bashecho "Running security checks..."
# Check for lockfileif [ ! -f "package-lock.json" ]; then echo "Error: package-lock.json not found" exit 1fi
# Verify lockfile matches package.jsonnpm ls --depth=0 > /dev/null 2>&1if [ $? -ne 0 ]; then echo "Error: Lockfile integrity mismatch" exit 1fi
# Run security auditif npm audit --audit-level=moderate; then echo "Audit passed"else echo "Audit failed - review vulnerabilities before proceeding" exit 1fi
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:
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: falseThis pipeline fails if:
npm auditfinds high-severity vulnerabilitiessocket scandetects known malicious packages- Lockfile integrity is broken
The .npmrc Security Configuration
I also added a .npmrc file for extra protection:
# Enable strict SSLstrict-ssl=true
# Always verify package integritypackage-lock=true
# Prefer offline packages (reduces network exposure)prefer-offline=true
# Never install packages without lockfilelockfile-version=3This 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
$ npm auditfound 0 vulnerabilitiesBut 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
$ npm audit fixup to date, audited 128 packages in 2sBut 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:
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 detectedEach layer catches what the previous might miss.
Related Knowledge: What Else to Consider
Typo-Squatting Attacks
Attackers create packages with names similar to popular ones:
Real package: axiosFake package: axios-pro, axios2, a-xios
Real package: expressFake package: expressjs, expres
Mistype one letter or add common suffix, get malwareAlways 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:
$ npm info axios{ name: 'axios', version: '1.7.9', maintainers: [ { name: 'axios-team', email: '...' } ], ...}Tools for Enhanced Protection
Two tools I now use:
- Socket.dev - Scans dependencies for known malicious patterns
- 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:
- Pin exact versions - Remove
^and~from package.json - Use npm ci - Strict lockfile adherence in CI/CD
- Run npm audit - But don’t rely on it alone
- Add security gates - Block builds with suspicious packages
- 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:
- 👨💻 axios GitHub Security Advisory
- 👨💻 npm ci Documentation
- 👨💻 Socket.dev: Dependency Security Scanner
- 👨💻 Aikido Security: Safe Chain for npm
- 👨💻 Reddit: 'Developers need to slow down on npm installs'
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments