Security

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.

March 9, 20266 min readShipSafer Team

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:

Capabilitynpm auditSnyk
CVE/advisory databasenpm Advisory DBSnyk DB + NVD + GitHub SA
License scanningNoYes
Fix PRsNoYes (auto PRs)
Reachability analysisNoYes (paid)
IDE integrationNoYes
Container scanningNoYes
# 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.

Check Your Security Score — Free

See exactly how your domain scores on DMARC, TLS, HTTP headers, and 25+ other automated security checks in under 60 seconds.