How to Audit npm Packages for Security
The average Node.js project pulls in hundreds of transitive dependencies. Any one of them can introduce a vulnerability, or worse, malicious code designed to steal credentials or environment variables. This guide covers the full audit workflow from basic npm audit to CI automation.
Step 1: npm audit
The built-in audit command checks your installed packages against the npm advisory database.
# Basic audit — shows vulnerabilities and severity
npm audit
# JSON output for scripting
npm audit --json | jq '.vulnerabilities | to_entries[] | {pkg: .key, severity: .value.severity, via: .value.via}'
# Audit only production dependencies (skip devDependencies)
npm audit --omit=dev
# Show the full dependency path for each vulnerability
npm audit --json | jq '.vulnerabilities | to_entries[] | {
pkg: .key,
severity: .value.severity,
fixAvailable: .value.fixAvailable,
nodes: .value.nodes
}'
Step 2: Fix Vulnerabilities
# Automatically fix vulnerabilities where a compatible update exists
npm audit fix
# Force updates even if they include breaking changes (review diff first)
npm audit fix --force
# Dry run — show what would change without applying
npm audit fix --dry-run
# Fix a specific package manually
npm update lodash --save
Not all vulnerabilities can be auto-fixed. When a fix would require a major version bump, you need to evaluate manually:
# Check what version is available and what changed
npm view lodash versions --json | jq '.[-5:]'
npm view lodash changelog
# Update and run your test suite
npm install lodash@4.17.21
npm test
Step 3: Socket.dev for Supply Chain Analysis
npm audit only checks known CVEs. Supply chain attacks (malicious code injected into a package) often appear before a CVE is filed. Socket analyzes package behavior.
# Install Socket CLI
npm install -g @socketsecurity/cli
# Scan your project
socket scan npm
# Check a specific package before installing
socket npm info malicious-looking-package
Socket flags:
- Install scripts — packages that run code during
npm install - Network access — packages that make HTTP requests
- Filesystem access — packages that read/write files
- Obfuscated code — packages that use eval or base64 strings
- Typosquatting — packages that look like popular ones
# Example output from socket scan:
# lodash@4.17.21: OK
# colors@1.4.0: WARNING — install script detected
# faker@5.5.3: CRITICAL — obfuscated code, network access
Step 4: Snyk for Vulnerability Scanning
Snyk maintains its own vulnerability database and catches issues npm audit misses.
# Install and authenticate
npm install -g snyk
snyk auth # opens browser, links to snyk.io account
# Scan current project
snyk test
# Monitor a project (alerts when new vulns are disclosed)
snyk monitor
# Fix vulnerabilities using Snyk's patches
snyk fix
# Scan a Docker image for npm vulnerabilities
snyk container test node:18-alpine
# In CI without a Snyk account — use the free tier API
snyk test --severity-threshold=high
# Exit code 1 if high or critical vulns found
Step 5: Detect Malicious Package Patterns
Beyond scanners, know what to look for manually:
# List all install scripts in your dependency tree
node -e "
const lock = require('./package-lock.json');
const packages = lock.packages || {};
Object.entries(packages)
.filter(([, v]) => v.scripts && (v.scripts.install || v.scripts.preinstall || v.scripts.postinstall))
.forEach(([name, v]) => console.log(name, v.scripts));
"
# Inspect a specific package's install script
cat node_modules/suspicious-pkg/package.json | jq '.scripts'
# Look for network calls in install scripts
grep -r "http\|fetch\|axios\|request\|curl\|wget" node_modules/suspicious-pkg/
# Inspect for obfuscated code patterns
grep -r "eval\|atob\|Buffer.from.*base64\|String.fromCharCode" node_modules/suspicious-pkg/ | head -20
Step 6: Lockfile Integrity
The package-lock.json file is your first line of defense. Commit it and verify it.
# Install only what's in the lockfile — no surprise updates
npm ci
# Never run npm install in CI (it can update the lockfile)
# Always use npm ci in CI environments
# Verify lockfile hasn't been tampered with
git diff HEAD package-lock.json
# Check if the lockfile matches package.json
npm ls --depth=0 2>&1 | grep "UNMET\|invalid"
Integrity hashes are stored in package-lock.json:
{
"node_modules/lodash": {
"version": "4.17.21",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZhr+33kbJlIdiwVRmA=="
}
}
npm verifies this hash on every install. If a package is modified after publish, the hash will not match.
Step 7: CI Integration
# .github/workflows/security.yml
name: Security Scan
on:
push:
branches: [main]
pull_request:
schedule:
- cron: '0 8 * * 1' # Weekly Monday morning scan
jobs:
npm-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: npm audit
run: npm audit --audit-level=high --omit=dev
- name: Socket scan
run: |
npm install -g @socketsecurity/cli
socket scan npm --strict
continue-on-error: true # change to false when baseline is clean
snyk:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high
Step 8: Dependency Review for Pull Requests
GitHub’s dependency review action blocks PRs that introduce vulnerable packages:
# .github/workflows/dependency-review.yml
name: Dependency Review
on:
pull_request:
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/dependency-review-action@v4
with:
fail-on-severity: high
deny-licenses: GPL-3.0, AGPL-3.0 # optional license policy
Auditing Existing Projects: Quick Script
#!/bin/bash
# npm-security-check.sh — run in any Node project
echo "=== npm audit ==="
npm audit --omit=dev --json | jq '{
total: .metadata.vulnerabilities.total,
critical: .metadata.vulnerabilities.critical,
high: .metadata.vulnerabilities.high
}'
echo ""
echo "=== Packages with install scripts ==="
node -e "
const lock = require('./package-lock.json');
const pkgs = lock.packages || {};
let count = 0;
Object.entries(pkgs).forEach(([name, v]) => {
if (v.scripts && (v.scripts.install || v.scripts.postinstall || v.scripts.preinstall)) {
console.log(' -', name);
count++;
}
});
console.log('Total:', count);
"
echo ""
echo "=== Dependencies older than 1 year ==="
npm outdated --json | jq -r 'to_entries[] | select(.value.current != .value.latest) | "\(.key): \(.value.current) → \(.value.latest)"' | head -20
Related Articles
- VPN Provider Annual Audit Results: Independent Security
- How to Audit Your Password Manager Vault: A Practical Guide
- How to Audit Your Cloud Storage Privacy
- How to Use Lynis for Linux Security Auditing
- How to Audit Docker Images for Vulnerabilities
- How to Audit What Source Code AI Coding Tools Transmit Built by theluckystrike — More at zovo.one