npm Security Audit: Finding and Fixing Vulnerable Dependencies
A complete workflow for npm security audits: npm audit, audit signatures, overrides for transitive vulnerabilities, Snyk comparison, and lockfile integrity checks.
npm Security Audit: Finding and Fixing Vulnerable Dependencies
Every npm install adds code written by people you have never met, running in your production environment with the same privileges as your own code. A single vulnerable transitive dependency — one you did not explicitly choose — can expose your application to remote code execution, data theft, or denial of service. npm's built-in audit tooling, combined with a few external tools and process discipline, gives you continuous visibility into this risk.
The Basics: npm audit
npm audit compares your installed packages against the npm Advisory Database and reports known vulnerabilities. Run it after every install:
npm audit
Sample output:
found 3 vulnerabilities (1 moderate, 2 high)
# Run `npm audit fix` to fix them, or `npm audit fix --force`
# to install breaking changes.
high severity vulnerability
Package semver
Patched in >=7.5.2
Dependency of my-tool
Path my-package > my-tool > semver
More info https://github.com/advisories/GHSA-c2qf-rxjj-qqgw
The output shows the package, the vulnerable version range, the patched version, and the dependency path. That last piece — the path — is critical for understanding whether this is a direct or transitive dependency.
npm audit fix
For vulnerabilities in direct dependencies with non-breaking fixes:
# Fix all non-breaking vulnerabilities
npm audit fix
# Fix including breaking changes (major version bumps) — review carefully
npm audit fix --force
# Preview what would change without applying
npm audit fix --dry-run
Use --force cautiously. Major version bumps can introduce API incompatibilities. Review the changelog for any package updated by --force.
npm audit --audit-level
In CI, you typically want to fail the build on high or critical vulnerabilities but not block on low/moderate:
# GitHub Actions
- name: Security audit
run: npm audit --audit-level=high
This exits with code 1 (failing the CI step) only if high or critical vulnerabilities are found.
Verifying Package Signatures
npm audit signatures (npm 8.x+) verifies that installed packages were actually published by the registry shown in your configuration, and that their contents have not been tampered with since publication. This is your defense against compromised packages and supply chain substitution:
npm audit signatures
Output for a clean install:
audited 847 packages in 3s
847 packages have verified registry signatures
If any packages cannot be verified (because they were published before npm added signature support, or because the signature is invalid), they appear as warnings. Unverifiable packages from unexpected registries should be investigated.
Handling Transitive Vulnerabilities with overrides
The most frustrating audit scenario is a high-severity vulnerability in a transitive dependency where the direct dependency has not yet shipped a fix. npm overrides (npm 8.3+) lets you force a specific version of a transitive dependency:
// package.json
{
"overrides": {
"semver": ">=7.5.2",
"minimist": "1.2.6"
}
}
This forces all occurrences of semver anywhere in your dependency tree to be at least 7.5.2. Use this as a temporary fix while waiting for upstream packages to update — it can introduce incompatibilities if the overridden version has a different API.
For a more targeted override (only affecting a specific package's transitive dependency):
{
"overrides": {
"my-tool": {
"semver": ">=7.5.2"
}
}
}
After adding overrides, run npm install to regenerate package-lock.json, then verify with npm audit.
Lockfile Integrity
Your package-lock.json (or npm-shrinkwrap.json) is a security artifact, not just a convenience. It pins every dependency to a specific version and records the integrity hash for each package. Never skip it or regenerate it carelessly.
# Use npm ci instead of npm install in CI — it validates the lockfile
npm ci
npm ci installs exactly what is in package-lock.json without updating it. If package.json and package-lock.json are out of sync, it fails. This makes it the right command for CI pipelines — you are validating the exact lockfile that was reviewed.
Detecting Lockfile Tampering
Check that your lockfile has not been modified unexpectedly:
# In CI, after npm ci, verify lockfile was not changed
git diff --exit-code package-lock.json
If this exits non-zero, the lockfile was modified by npm ci, which should not happen. Investigate.
Snyk vs npm audit
Snyk provides a superset of what npm audit offers:
| Capability | npm audit | Snyk |
|---|---|---|
| CVE/advisory database | npm Advisory DB | Snyk DB + NVD + GitHub SA |
| License scanning | No | Yes |
| Fix PRs | No | Yes (auto PRs) |
| Reachability analysis | No | Yes (paid) |
| IDE integration | No | Yes |
| Container scanning | No | Yes |
# Install and run Snyk
npm install -g snyk
snyk auth
snyk test
# Monitor continuously (sends results to Snyk dashboard)
snyk monitor
Snyk's advisory database often has earlier coverage of new vulnerabilities than the npm Advisory Database. Its fix PR feature automatically opens pull requests to update vulnerable packages, which reduces the manual work of remediation.
For most teams, running both npm audit (free, built-in) and snyk test (free tier available) in CI gives the best coverage.
Automating with Dependabot
GitHub's Dependabot automates dependency updates and security patches:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
groups:
production-dependencies:
patterns:
- "*"
exclude-patterns:
- "eslint*"
- "@types/*"
Dependabot opens pull requests for security updates immediately (not waiting for the weekly schedule) and groups non-security updates into batches to reduce noise.
Building a Sustainable Audit Workflow
A one-time audit is not enough. Vulnerabilities are disclosed continuously, and a package that was clean yesterday may have a CVE today. Build this into your workflow:
# GitHub Actions — run on every PR and daily
on:
pull_request:
schedule:
- cron: '0 8 * * *'
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm audit --audit-level=high
- run: npm audit signatures
- run: npx snyk test --severity-threshold=high
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
The daily schedule catches newly disclosed CVEs for packages that were previously clean. The PR check prevents introducing new vulnerabilities during development. Together they close the vulnerability window to its minimum.