Scan free

How to Fix npm Vulnerabilities Without Breaking Your App

Here's how to fix npm vulnerabilities without breaking your app: don't start with npm audit fix --force. Run npm audit --omit=dev to see what actually ships to production, fix anything attackers are already exploiting first, then let npm audit fix handle the safe upgrades and deal with the rest one package at a time, on a branch, with your tests running.

That order matters because the number npm prints after an install is not a to-do list. Here is what npm install printed for a small Express app with an older test runner, set up for this post:

text
15 vulnerabilities (4 low, 11 high)

To address issues that do not require attention, run:
  npm audit fix

To address all issues (including breaking changes), run:
  npm audit fix --force

Eleven "high" findings at 11pm reads like an emergency. Seven of those eleven turned out to be in the test runner, which never runs in production. The rest of this post is how to tell the difference, and what to do about each kind.

How to fix npm vulnerabilities: the short version

Triage before you fix. Audit only production dependencies, trace each finding to the package you installed, and fix in order: anything on CISA's Known Exploited Vulnerabilities catalog, then reachable production packages by severity, then dev tooling. Use npm audit fix for compatible updates, upgrades or overrides for the rest, and commit the lockfile.

The steps, in order:

  1. See what's real: npm audit --omit=dev, then npm explain <package> for each finding.
  2. Prioritize: known-exploited first, then production packages your app reaches, by severity, then dev-only tooling.
  3. Fix: npm audit fix, then upgrade the direct dependency, then overrides for a stubborn transitive one. Use --force only on a branch.
  4. Handle "No fix available": replace, override, remove, or accept it with a written note.
  5. Verify and lock: re-run the audit, commit package.json and package-lock.json together.

Step 1: see which vulnerabilities are real

npm audit production only

npm audit checks everything in your lockfile, including linters, test runners and build tools that never reach a user. To audit production dependencies only, omit the dev ones:

bash
npm audit --omit=dev

On the same project, that took the count from 15 to 7. Per the npm audit docs, omitted dependency types are skipped when the report is generated. Your lockfile doesn't change.

One caveat. --omit=dev trusts how your package.json is split, and a bundler doesn't care which section a package sits in. If a library is listed under devDependencies but your frontend imports it, it ships in your JavaScript bundle anyway. Glance at your dev list before you write those findings off.

Read the path with npm explain

Each finding names a package you probably never installed yourself. npm explain (alias npm why) prints the chain that pulled it in:

bash
npm explain cookie
text
cookie@0.4.0
node_modules/cookie
  cookie@"0.4.0" from express@4.17.1
  node_modules/express
    express@"4.17.1" from the root project

Now you know two things. cookie is a transitive dependency, and the thing you control is express. Most fixes start from the package at the bottom of that chain, the one "from the root project".

Step 2: decide what to fix first

Severity isn't exploitability

The severity label (low, moderate, high, critical) describes how bad a flaw would be if someone used it. It says nothing about whether anyone is using it, or whether your app even calls the vulnerable code. FIRST, which maintains CVSS, says it plainly in the CVSS user guide: base scores measure severity and should not be used alone to assess risk.

A "high" regular-expression denial of service in a file-matching library your test runner uses is a different problem from a "high" in the request parser every API call goes through. Same label, very different urgency.

Anything attackers are already exploiting comes first

CISA, the US cybersecurity agency, keeps the Known Exploited Vulnerabilities catalog: vulnerabilities with evidence of exploitation in the wild. If a package in your app has a CVE on that list, attackers are already exploiting it, and it goes to the top regardless of what else is open.

It's a short list, 1,748 entries across all software in the October 16, 2026 version, so most of what npm audit prints won't be on it. JavaScript packages do show up, though. React's Server Components remote code execution flaw, CVE-2025-55182, was added on 2025-12-05 and affected Next.js among other frameworks (React's advisory lists the fixed versions).

To check a finding yourself: npm audit links each advisory as a GitHub GHSA- id. Look up its CVE id, then search the catalog for it:

bash
curl -s https://api.github.com/advisories/GHSA-pxg6-pf52-xh8x | jq -r .cve_id

curl -s https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json \
  | jq -r '.vulnerabilities[] | select(.cveID == "CVE-2025-55182") | "\(.cveID) added \(.dateAdded), ransomware: \(.knownRansomwareCampaignUse)"'

No output from the second command means that CVE isn't on the catalog.

Then reachable production packages, by severity

After the known-exploited ones, work through production findings from critical down. For each, ask whether your app reaches the vulnerable code: a denial-of-service bug in URL-encoded form parsing matters if you parse forms, less if you don't. If you can't tell within a few minutes, treat it as reachable. Upgrading is usually cheaper than proving it's unreachable.

Dev-only tooling last

Vulnerabilities in test runners, linters and bundlers matter on your machine and in CI, not in the deployed app. Fix them when you have a quiet afternoon, not at 11pm before launch.

Step 3: fix it

npm audit fix

Start with the safe one:

bash
npm audit fix --dry-run
npm audit fix

npm audit fix only moves packages within the version ranges in your package.json, so it applies SemVer-compatible updates. Compatible is a promise from the package author, not a guarantee, so run your tests and click through the app afterwards. Then commit the updated lockfile.

Upgrade the direct dependency

When the vulnerable package is transitive, the clean fix is usually a newer version of the package that pulled it in. In the example above, newer Express 4 releases ship a patched cookie:

bash
npm install express@4
npm explain cookie

express@4 installs the newest 4.x release, which avoids a jump to a new major version. Read the package's changelog when you move more than a patch or two.

How to fix npm vulnerabilities manually with overrides

Sometimes the parent package hasn't released a fix yet, or you can't upgrade it this week. npm's overrides field in your root package.json forces a version of a package anywhere in the tree:

json
{
  "dependencies": {
    "express": "4.17.1"
  },
  "overrides": {
    "cookie": "^0.7.0"
  }
}

Run npm install afterwards so the lockfile picks it up. npm explain cookie should now say overridden cookie@"^0.7.0" (was "0.4.0").

Three things to know. Overrides only work in the root package.json. You can't override a package you depend on directly unless the specs match, so use a normal upgrade for those. And you're now running the parent with a version of its dependency it wasn't released with, so test the parts of your app that use it, and remove the override once the parent ships its own fix.

npm audit fix force: what it actually does

The npm docs describe --force as allowing npm audit fix to "install modules outside your stated dependency range (including SemVer-major changes)". In practice it reads like this:

text
fix available via `npm audit fix --force`
Will install mocha@12.0.3, which is a breaking change
text
fix available via `npm audit fix --force`
Will install express@4.22.3, which is outside the stated dependency range

The second one is a minor-version bump, but it still needs --force because package.json pinned the exact version 4.17.1. That's why --force breaks apps: it rewrites your version choices in one go, across every package it touches, without asking which ones you meant.

Is npm audit fix safe?

Plain npm audit fix is usually safe, provided you test afterwards. npm audit fix --force is safe only when you treat it as an upgrade project:

  1. Create a branch: git switch -c fix/npm-audit.
  2. Run npm audit fix --force --dry-run and read every "Will install" line.
  3. Run npm audit fix --force.
  4. Run npm test and npm run build, then use the app: sign in, the main flow, payments.
  5. Check the changelog of each package that jumped a major version for breaking changes.
  6. Merge only when it all works. If it doesn't, upgrade those packages one at a time instead.

Step 4: npm audit no fix available

"No fix available" means no released version of the chain resolves the advisory. Here's part of what the deprecated request library printed in the same test project:

text
form-data  <=2.5.5
Severity: critical
form-data uses unsafe random function in form-data for choosing boundary - https://github.com/advisories/GHSA-fjxv-7rqg-78g4
No fix available
node_modules/form-data
  request  *
  Depends on vulnerable versions of form-data

Your options, roughly in order of preference:

  • Replace the package. request is deprecated; Node 18 and later ship a built-in fetch.
  • Override the vulnerable transitive package to a patched version, if one exists and the parent still works with it.
  • Remove the package if nothing imports it any more. Generated code can leave packages behind after a refactor, so search before you assume it's needed.
  • Accept it, with a note: which advisory, why your app doesn't reach it, and a date to look again.

Can I ignore npm vulnerabilities?

Some of them, yes, deliberately. Dev-only findings and ones your code can't reach are reasonable to leave, as long as you write down why. npm has no built-in ignore list, so the note lives in your repo or your pull request.

What you can control is when CI fails. The docs are explicit that --audit-level "does not filter the report output, it simply changes the command's failure threshold":

bash
npm audit --omit=dev --audit-level=high

That fails the build on high or critical production findings and lets lower ones through.

npm audit only critical

Since --audit-level doesn't hide anything, filter the JSON report instead:

bash
npm audit --omit=dev --json \
  | jq -r '.vulnerabilities[] | select(.severity == "critical" or .severity == "high") | "\(.severity)\t\(.name)\tdirect=\(.isDirect)"'

npm audit fix not working

If npm audit fix runs and the count barely moves, it's usually one of these:

  • The fix needs a major version, or your package.json pins exact versions, so only --force or a manual upgrade will apply it.
  • No fix exists yet (see above).
  • A parent package pins the vulnerable version exactly, so nothing changes until you upgrade the parent or add an override.
  • There's no package-lock.json. npm needs one to audit by default. Projects on pnpm or Yarn should use those tools' own audit commands.

Step 5: verify and lock it in

Re-run the audit and confirm what's left is what you decided to leave:

bash
npm audit --omit=dev
git add package.json package-lock.json
git commit -m "Fix production npm vulnerabilities"

Commit the lockfile every time. It records the exact versions you tested. Without it, the next install can resolve different versions, and anything that checks your repository for vulnerable packages has nothing exact to check. In CI, install with npm ci so the build uses that lockfile as written.

This is also where a scan helps between audits. Tuvio's free scan, no account needed, reads the lockfile in your GitHub repository (JavaScript is one of 13 ecosystems it covers), reports known vulnerabilities in production dependencies, and marks the ones on CISA's catalog, listing them first within their severity. The catalog and the vulnerability database behind it are refreshed weekly. A package.json with no lockfile isn't read, and neither is a binary bun.lockb on its own, so the lockfile you just committed is what gets checked. What a scan covers lists the rest.

Checklist

  • Run npm audit --omit=dev and ignore the install-time total.
  • Trace each finding with npm explain to the package you control.
  • Check CVE ids against CISA's catalog; fix those first.
  • Fix production findings by severity, reachable ones first.
  • npm audit fix, then upgrades, then overrides; --force only on a branch with tests.
  • For "No fix available": replace, override, remove, or accept with a note.
  • Commit package.json and package-lock.json together.
  • Add npm audit --omit=dev --audit-level=high to CI.

Dependencies are one item on a longer list. The vibe coding security checklist covers the rest, and if a key ended up in your repository along the way, start with what to do after pushing a .env file.