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:
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 --forceEleven "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:
- See what's real:
npm audit --omit=dev, thennpm explain <package>for each finding. - Prioritize: known-exploited first, then production packages your app reaches, by severity, then dev-only tooling.
- Fix:
npm audit fix, then upgrade the direct dependency, thenoverridesfor a stubborn transitive one. Use--forceonly on a branch. - Handle "No fix available": replace, override, remove, or accept it with a written note.
- Verify and lock: re-run the audit, commit
package.jsonandpackage-lock.jsontogether.
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:
npm audit --omit=devOn 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:
npm explain cookiecookie@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 projectNow 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:
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:
npm audit fix --dry-run
npm audit fixnpm 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:
npm install express@4
npm explain cookieexpress@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:
{
"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:
fix available via `npm audit fix --force`
Will install mocha@12.0.3, which is a breaking changefix available via `npm audit fix --force`
Will install express@4.22.3, which is outside the stated dependency rangeThe 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:
- Create a branch:
git switch -c fix/npm-audit. - Run
npm audit fix --force --dry-runand read every "Will install" line. - Run
npm audit fix --force. - Run
npm testandnpm run build, then use the app: sign in, the main flow, payments. - Check the changelog of each package that jumped a major version for breaking changes.
- 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:
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-dataYour options, roughly in order of preference:
- Replace the package.
requestis deprecated; Node 18 and later ship a built-infetch. - 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":
npm audit --omit=dev --audit-level=highThat 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:
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.jsonpins exact versions, so only--forceor 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:
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=devand ignore the install-time total. - Trace each finding with
npm explainto 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, thenoverrides;--forceonly on a branch with tests.- For "No fix available": replace, override, remove, or accept with a note.
- Commit
package.jsonandpackage-lock.jsontogether. - Add
npm audit --omit=dev --audit-level=highto 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.