Triage Dependency Vulnerabilities Without Panic
12 May 2026npm audit reports 40 vulnerabilities and your instinct is either to panic-update everything or ignore the whole list. Both are wrong. Most of those alerts don’t affect you, and figuring out which ones do takes ten minutes, not a security review.
Start with severity, but don’t stop there
npm audit --audit-level=high
This filters out the low/moderate noise, which is most of it. But severity alone is misleading — a “critical” vulnerability in a package whose vulnerable function you never call is not critical to you. Severity describes the CVE in the abstract, not your exposure.
The question that actually matters: is the vulnerable code path reachable?
npm audit --json | jq '.vulnerabilities | to_entries[] | select(.value.severity == "critical" or .value.severity == "high") | {name: .key, via: .value.via}'
For each result, check: is this a direct dependency, or nested three levels deep inside something you don’t call directly? Run:
npm ls <package-name>
to see the dependency chain. If it’s nested inside a devDependency that only runs at build time and never ships to production — say, a vulnerable transitive dep of your test runner — your actual exposure is close to zero. If it’s in a production dependency handling user input, that’s the one to act on today.
A practical triage table
The severity-then-reachability check above collapses into a simple decision:
flowchart TD
A["High/critical CVE"] --> B{"Direct prod\ndependency?"}
B -- No --> C{"devDependency\nonly, no prod exposure?"}
C -- Yes --> D["Fix on next routine update"]
C -- No --> E["Watch, don't block"]
B -- Yes --> F{"Is the vulnerable\nfunction in your\ncall path?"}
F -- Yes --> G["Fix now"]
F -- No --> H["Fix this sprint"]
| Situation | Priority |
|---|---|
| Direct prod dependency, vulnerable function is in your call path | Fix now |
| Direct prod dependency, vulnerable function you never call | Fix this sprint |
| Transitive dependency, no fix available yet | Watch, don’t block |
| devDependency only, no production exposure | Fix on the next routine update |
| “Denial of service” CVE on an internal tool with no public exposure | Low priority |
Read the actual advisory, not just the headline
npm audit --json | jq '.vulnerabilities["package-name"].via[0]'
GitHub’s advisory database (the source most tools pull from) usually names the specific function or code path affected. A prototype pollution CVE in a templating library only matters if you pass untrusted input into that specific function — many don’t in typical usage.
When there’s no patch yet
Options in order of preference:
- Pin to a version just before the vulnerable one was introduced, if the CVE was recently added in a newer release.
- Check if a patched fork or drop-in replacement exists — common for abandoned packages.
- Mitigate at your layer — if the vulnerability requires a specific input pattern, validate against it at your boundary regardless of the library’s fix status.
- Accept the risk explicitly, documented, with a re-check date — not silence, an actual decision written down.
Automate the noise away, not the decisions
Dependabot or Renovate opening a PR for every patch bump is good. Auto-merging every PR without review is not — a patch version bump can still break your build (semver is a convention, not a guarantee). A reasonable middle ground:
# renovate.json
{
"packageRules": [
{ "matchUpdateTypes": ["patch"], "automerge": true, "matchDepTypes": ["devDependencies"] },
{ "matchUpdateTypes": ["major"], "automerge": false }
]
}
Auto-merge patch bumps on dev tooling where a bad update just breaks your local build, not your production system. Keep a human in the loop for anything touching production dependencies, especially major version bumps.
The concrete next action
Run npm audit --audit-level=high right now. For anything critical or high, spend two minutes checking npm ls before deciding it’s an emergency — most of the list turns out not to be one.