Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild
Back to Blog Explainer

npm Vulnerability Reports: Which Ones Actually Matter

Sean

Platform Writer

Aug 30, 2026
8 min read

npm audit reports every known advisory in your dependency tree without knowing whether the vulnerable code is reachable. A critical in a build-only dependency is usually not a critical in your application.

npm Vulnerability Reports: Which Ones Actually Matter

Every developer has run npm install on a fresh project and been told about 47 vulnerabilities, 12 of them critical, in a project that does nothing yet. The number is real and the alarm is mostly misplaced.

The useful skill is triage: separating advisories that describe a genuine risk to your running application from ones that describe a theoretical risk in a tool that runs on your machine at build time.

Table of contents

What npm audit is doing

It sends your dependency tree to the registry, compares it against the advisory database, and reports every match. It does not analyse your code, so it cannot know:

  • Whether the vulnerable function is ever called.
  • Whether the dependency runs in production or only during the build.
  • Whether your usage matches the exploit conditions in the advisory.
  • Whether the attack requires input you never pass to it.

It is a report of presence, not of exposure. That is a genuinely useful thing to have and it is not a risk assessment.

Start by splitting the report:

npm audit --omit=dev      # what actually ships
npm audit                 # everything, including build tooling
npm audit --json | jq '.vulnerabilities | keys'

--omit=dev is the first useful filter and often cuts the count dramatically. A vulnerability reachable at runtime by user input is a different thing from one in a test framework.

Reading an advisory properly

Four questions, in order:

  1. Is it a runtime or dev dependency? A ReDoS in a linter that runs on your own source is not a production risk.
  2. What is the attack vector? Many advisories require attacker-controlled input to a specific function. If you call it with a hardcoded value, or never call it, exposure is nil.
  3. Is the vulnerable path reachable? A vulnerability in an optional feature of a library you use narrowly may not be in your code path at all.
  4. Is there a fix? An advisory with no patched version needs a different response from one with a patch available.

The classic example is ReDoS — a regular expression that can be made to run for a very long time. Genuinely serious if the regex processes user input on a request path. Irrelevant if it parses your own configuration at build time. Same advisory, same severity rating, completely different risk.

Also relevant: severity in the database is assigned to the worst case, not to your case. A critical in a package used in a way the exploit does not apply to is not critical for you. Record that judgement somewhere, because you will otherwise re-derive it every time the audit runs.

Fixing without breaking

npm audit fix            # semver-compatible upgrades only -- safe
npm audit fix --force    # ignores semver, installs breaking changes

npm audit fix is safe by design: it only makes upgrades allowed by your existing version ranges.

--force is not. It installs major version bumps regardless of your declared ranges, and it will happily downgrade a package to an older version that has no advisory but also has none of the features you depend on. Running it on a Friday is a well-established way to spend a weekend on a build failure.

If you do use it, do it on a branch with the tests running, and read the diff to package.json before committing.

For a transitive dependency where the direct parent has not updated, overrides forces a version through:

{
  "overrides": {
    "vulnerable-package": "^2.0.0"
  }
}

This is a genuine escape hatch and a sharp one — you are asserting the parent works with a version it never declared support for. Test properly afterwards.

When there is no fix

Sometimes an advisory has no patched version, or the fix requires a major upgrade you cannot take yet. The options, in order of preference:

  1. Assess and document. If the vulnerable path is unreachable in your usage, record why. That is a legitimate resolution, not a dodge — but write it down, or it gets re-litigated monthly.
  2. Mitigate around it. Validate input before it reaches the vulnerable code, or constrain what can be passed.
  3. Replace the dependency. Often the honest answer for an unmaintained package, and worth considering regardless of the advisory.
  4. Vendor and patch. patch-package applies a local fix to node_modules. A maintenance burden, but sometimes the only way through.
  5. Accept and monitor. Explicitly, with a review date.

What not to do is add a blanket exclusion and forget. If you suppress an advisory, put a date on it and revisit.

A useful CI setting is a severity threshold, so builds fail on things that matter rather than on everything:

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

Reducing the surface

The durable fix is fewer dependencies. Every package added brings its own tree, and the transitive count grows faster than anyone expects.

Habits that help:

  • Check what a package costs before adding it. A dependency pulling 40 transitive packages for one function is a bad trade — write the function.
  • Prefer the standard library. Node covers a lot now that used to need a package: fetch, structuredClone, crypto.randomUUID, node:test.
  • Commit the lockfile and use npm ci in CI. npm ci installs exactly the lockfile, so builds are reproducible and a compromised release cannot slip in through a range.
  • Automate updates. Small, frequent dependency bumps are far easier to review than an annual one that moves everything at once.

The last point is the one with the most leverage. Most difficult upgrades are difficult because they were deferred, and a routine that reviews one small update a week never accumulates that debt.

What actually ships

Worth being concrete about the production picture. A multi-stage build that installs dev dependencies for the build and copies only production dependencies into the final image means most of what npm audit reports is not in the running container at all:

FROM node:22-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/server.js"]

That is a smaller image, a smaller attack surface, and a much shorter list of advisories that describe code actually running in production.

Node services deploy on RunxBuild from a repository with this build running in the platform’s build step, the output visible in the build log, and rollback to a previous deploy if a dependency upgrade turns out to break something at runtime rather than at build time.

How this fits the rest of the stack

npm audit reports presence, not exposure — start with --omit=dev, read the attack vector, and judge reachability rather than reacting to the severity label. Use npm audit fix freely and --force only on a branch with tests. The durable improvements are fewer dependencies, npm ci against a committed lockfile, and small frequent updates. The RunxBuild hosting calculator covers what the service running that trimmed-down build costs.

Useful related references:

FAQ

Should I fix every npm audit vulnerability?

No. npm audit reports every known advisory in your tree without knowing whether the vulnerable code is reachable. Start with npm audit --omit=dev, then assess each remaining one by attack vector and whether your code path can reach it.

Is npm audit fix —force safe?

No. It ignores your declared version ranges and installs breaking major upgrades, and can downgrade packages to older versions without the features you rely on. Run it on a branch with tests, review the package.json diff, and never on a Friday.

What does —omit=dev do in npm audit?

It excludes development dependencies, so the report covers only packages that ship to production. This usually cuts the count substantially, because build tooling, linters and test frameworks account for a large share of reported advisories.

How do I fix a vulnerability in a transitive dependency?

If the direct parent has not updated, use the overrides field in package.json to force a patched version through. This asserts compatibility the parent never declared, so test thoroughly afterwards.

What if there is no patched version available?

Assess whether the vulnerable path is reachable in your usage and document the conclusion, mitigate by validating input before it reaches the code, replace the dependency, or patch it locally with patch-package. Whatever you choose, set a review date rather than suppressing it permanently.

#npm audit#npm vulnerability#dependency security#supply chain#Node.js