Remediation guide

How to fix npm audit vulnerabilities

You ran npm audit and now you're looking at a wall of findings — critical, high, moderate — spread across packages you installed and packages you've never heard of. This guide walks through every remediation path, in the order you should try them.

Don't reach for --force yet. npm audit fix --force can silently upgrade packages across major versions and break your app. Read through the steps below first — most vulnerabilities have a safer fix.

Reading the npm audit output

Before fixing anything, understand what the report is telling you. Each advisory contains: the severity, the package name, the vulnerable version range, the fixed version (if one exists), and whether the package is a direct dependency (one you installed) or a transitive dependency (installed by one of your packages). This distinction determines which fix applies.

SeverityWhat it typically meansTypical action
CriticalRemote code execution, authentication bypass, or arbitrary file access with no special conditionsFix before the next deploy
HighSignificant data exposure, privilege escalation, or DoS under realistic conditionsFix within a day or two
ModerateExploitable but usually requires specific conditions (e.g., attacker-controlled input you may not accept)Fix in next scheduled update, sooner if reachable
LowMinimal real-world impact; theoretical or requires extensive preconditionsBatch with routine updates

The JSON report (npm audit --json) gives you the full advisory text and the dependency chain for each finding — useful when you need to trace a transitive vulnerability back to which of your direct dependencies is pulling it in.

Step 1: Run npm audit fix (the safe path)

Start here. npm audit fix upgrades vulnerable packages within your existing semver constraints — for example, if you depend on "lodash": "^4.0.0" and version 4.17.21 contains a fix, it upgrades to that without touching your declared range or breaking your API contract.

# Run the safe auto-fix
npm audit fix

# Then re-check — not everything will be fixed
npm audit

# Commit your updated package-lock.json
git add package-lock.json
git commit -m "chore: npm audit fix"

After running, commit the updated package-lock.json. Your code should be tested and redeployed — even patch upgrades can occasionally introduce regressions.

If vulnerabilities remain after this step (they usually will), continue below. The remaining cases require more targeted intervention.

Step 2: Manual targeted upgrades for direct dependencies

For vulnerabilities that npm audit fix didn't resolve because they require a version jump outside your declared range, upgrade the specific package directly. The audit output shows the fixed version — install it explicitly:

# Install a specific fixed version
npm install package-name@4.2.1

# Or upgrade to latest stable
npm install package-name@latest

# Check again
npm audit

After installing, check that your app still works. The jump to a new major version may have changed APIs. Review the package's changelog between your old version and the new one to understand what might have changed.

When is npm audit fix --force appropriate? Only after you understand exactly what it will change. Run npm audit fix --force --dry-run first — it prints what would happen without making any changes. If the proposed upgrades look reasonable and you have test coverage, then apply it. Immediately run npm test and verify your app manually.

Step 3: Fix transitive dependency vulnerabilities with overrides

Transitive vulnerabilities are the most frustrating case. The vulnerable package isn't in your package.json — it's a dependency of a dependency. npm audit fix can't resolve these if the parent package hasn't released an upgrade yet.

You have two main options:

Option A: Upgrade the parent package

First, identify which of your direct dependencies is pulling in the vulnerable transitive package. The audit report's dependency chain shows this. Upgrade that parent to its latest version — it may have already updated its own dependency tree to a safe version, even if there's no explicit advisory about it.

# Find the dependency chain (JSON gives the full path)
npm audit --json | npx node -e "
  const d=require('fs').readFileSync('/dev/stdin','utf8');
  const a=JSON.parse(d);
  Object.values(a.vulnerabilities||{}).forEach(v=>
    console.log(v.name, v.via?.map?.(x=>typeof x==='string'?x:x.name)));
"

# Upgrade the parent package
npm install parent-package@latest

Option B: Force a specific version with overrides (npm v8.3+)

If the parent package hasn't released a fix yet, you can force the entire dependency tree to use a specific (safe) version of the transitive package by adding an overrides field to your package.json:

{
  "dependencies": { ... },
  "overrides": {
    "vulnerable-transitive-package": "^2.1.4"
  }
}

After adding the override, run npm install to regenerate the lockfile, then npm audit to confirm the finding is resolved.

Overrides can break the parent package. If the parent relies on behavior that changed between versions, forcing a different version can cause subtle runtime failures. Test thoroughly after applying overrides. If you're using Yarn, the equivalent is resolutions; for pnpm, it's pnpm.overrides.

Step 4: Vulnerabilities with no fix available yet

Some advisories are published before a patch exists. The advisory's "Patched in" field will be empty, or you'll see a notice that no fix is available. Here's what to do:

  • Check the package's GitHub issues. Maintainers often comment on the timeline for a fix. Sometimes a patched version is already in a PR waiting to be released.
  • Assess real-world reachability. A regular-expression DoS vulnerability matters a lot if your app applies that regex to user-supplied input. It matters less if the regex only runs on your own hardcoded configuration. Read the advisory's description to understand what conditions an exploit requires.
  • Check for a patched fork. Sometimes a community fork has already backported the fix. Use it via a GitHub dependency reference ("package": "github:org/fork#commit-sha") if a fix is urgent.
  • Document the accepted risk explicitly. If you decide to wait, add a comment in your package.json or a tracking issue. An unpatched vulnerability that was consciously accepted is very different from one that was overlooked.

If the package is abandoned with no maintainer activity and no fix coming, the right path is replacement — see Step 5.

Step 5: Replace abandoned packages

A package with an unpatched CVE and a dormant GitHub repo is a liability with no exit ramp via upgrade. The fix is to remove the dependency and replace it with a maintained alternative. Common examples:

  • Request-related utilities (e.g., request, deprecated since 2020) → replaced by node-fetch, undici, or the native fetch in Node 18+
  • Old HTML parsing libs → replaced by cheerio (actively maintained) or parse5
  • Crypto utilities with outdated algorithms → native crypto module where possible

npm audit tells you a package has a CVE but doesn't tell you it's unmaintained. That's a gap. Paste your package.json into DepCheck to see which of your dependencies are abandoned, have no recent releases, or are flagged as deprecated — giving you the full picture beyond just known CVEs.

Check your dependencies with DepCheck →

Prevention: catch vulnerabilities before they reach production

  • Run npm audit in CI. By default, npm audit exits with a non-zero code if any vulnerabilities are found. Use npm audit --audit-level=high to fail only on high and critical findings, letting low and moderate pass while you address them on a schedule.
  • Commit your lockfile. package-lock.json pins exact transitive versions so that npm install on any machine produces the same tree. Without it, a fresh install might pull in a newer (or older, or compromised) transitive version.
  • Schedule regular npm outdated reviews. Many vulnerabilities are fixed in patch or minor releases that are already compatible with your semver ranges — they just haven't been pulled in since your last install. A weekly or bi-weekly npm outdated && npm update cycle keeps your lockfile current.
  • Check for risky dep patterns beyond CVEs. npm audit only surfaces published advisories. It won't flag a package that was deprecated last month, a typosquatted name that resembles a popular utility, or a copyleft license that conflicts with your commercial use. DepCheck covers those gaps.
  • Limit your dependency footprint. Every added package is a new attack surface. Before installing a new package, ask whether it's really necessary — or whether a few lines of code would do the same job without the ongoing maintenance burden.

Frequently asked questions

Why does npm audit fix not fix all vulnerabilities?

npm audit fix only applies upgrades that are semver-compatible with your declared ranges. If the fix requires a major version bump (e.g., 1.x → 2.x), npm won't apply it automatically because that's outside the scope of a ^ constraint. Similarly, if the vulnerability is in a transitive package and the parent hasn't released an upgrade yet, there's nothing for npm to install. For those cases, use manual upgrades or overrides.

Is npm audit fix --force safe to run?

Only with caution. --force allows npm to upgrade packages beyond your semver constraints, including major version bumps that can introduce breaking API changes. Before using it, run npm audit fix --force --dry-run to preview the changes. After applying, run your test suite in full — at a minimum, verify the app starts and the critical paths work.

What is a transitive vulnerability and why can't I just update it directly?

A transitive (or indirect) dependency is a package installed by one of your dependencies — not something you added yourself. Because it's not in your package.json, running npm install vulnerable-package@safe-version won't work — npm won't treat it as your dependency. You either upgrade the parent package that pulls it in, or force a specific version using the overrides field.

Should I fix low and moderate severity findings?

It depends on whether the vulnerability is reachable in your app. A moderate-severity prototype pollution vulnerability matters if the library processes JSON from an untrusted source; it matters far less in a dev-only CLI tool that only runs locally. Read the advisory to understand the attack conditions. As a general rule: fix anything reachable by user input regardless of severity, and batch non-reachable low/moderate findings into routine updates.

What do I do if there's no fix available?

Check the package's GitHub issues for a patch timeline or a community fork with the fix backported. Assess whether your app actually exercises the vulnerable code path. If the package is abandoned, find a maintained replacement. If you decide to live with the risk temporarily, document that decision explicitly so it doesn't get lost. Don't just silence the warning with npm audit --omit=optional and move on.

How do I stop npm audit from blocking CI on low-severity findings?

Use the --audit-level flag: npm audit --audit-level=high exits non-zero only for high and critical vulnerabilities. Low and moderate findings will appear in the report output but won't fail the build. This lets you maintain CI hygiene while addressing lower-severity issues on a scheduled cadence.

Also in the Copper Bay Labs ship-safety suite

Vulnerable dependencies are one of several things to catch before you ship. These free tools cover the rest: