Outdated dependencies are a risk surface, not just technical debt
By Seaworthy · 9 June 2026
There is a stat that should make you uncomfortable: the average time between a CVE being published and it being actively exploited in the wild is now under a week for high-severity vulnerabilities. Some critical ones get weaponised within 24 hours of disclosure. Meanwhile, the median npm package in a production Node.js app has not been updated in over a year.
That gap is not technical debt. It is an open window.
The exploitation window is not what it used to be
Five years ago, you had a few weeks after a CVE dropped before you needed to worry about opportunistic exploitation. That gave teams time to triage, test, and deploy a fix in their normal release cadence. That luxury is gone. The security research community publishes proof-of-concept exploit code faster, vulnerability databases are scraped continuously by automated scanners, and botnets are configured to start testing for new CVEs within hours of disclosure.
This matters for dependencies specifically because your node_modules tree is enormous. A typical React app has 800-1200 transitive dependencies. You wrote none of them. You probably reviewed none of them. And every one of them is a potential entry point.
What npm audit and npm outdated actually tell you
Run these two commands in any production codebase right now:
npm audit --json | jq '[
.vulnerabilities | to_entries[] |
select(.value.severity == "critical" or .value.severity == "high") |
{ package: .key, severity: .value.severity, fixAvailable: .value.fixAvailable }
] | sort_by(.severity)'
The --json flag gives you machine-readable output. Piping through jq filters down to just the critical and high findings, whether a fix is actually available, and sorts by severity. The raw npm audit output is noisy; this cuts through it.
Then run:
npm outdated --long 2>/dev/null | awk 'NR==1 || $3 != $2 {print}'
This shows packages where the installed version differs from the wanted version (per your semver ranges) and the latest available. The gap between wanted and latest tells you how far your ranges have drifted from reality.
Here is what most teams miss: npm audit only reports packages with known CVEs. A package that is 18 months old and has a critical vulnerability that has not been assigned a CVE yet will not appear. npm outdated catches that second category, because it just looks at version age regardless of disclosure status.
Triaging the update queue
Not all updates are equal risk. When I am working through a backlog of outdated packages, I sort them into three buckets:
Patch updates with a security advisory: do these immediately, same day if possible. No triage needed. The fix exists, the risk is quantified, deploy it.
Minor version updates from packages in your critical path (auth libraries, HTTP clients, anything that touches user input): block time in your next sprint. Read the changelog. Test.
Major version bumps: treat these like any other migration. They deserve a branch, a real test run, and the same review process you would give to new code. Do not batch major updates; update one package at a time so you can isolate breakage.
The worst thing you can do is let the queue build up until you have 200 outdated packages, panic, run npm update blindly, and ship without testing. I have seen that cause more downtime than the vulnerabilities themselves.
Automating without drowning in PRs
Both Dependabot and Renovate can automate this, but their default configurations will spam you with a PR per package per day. That noise trains your team to ignore the PRs entirely, which defeats the purpose.
Here is a Renovate config that I actually find usable:
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"schedule": ["every weekend"],
"prConcurrentLimit": 5,
"grouping": [
{
"matchPackagePatterns": ["eslint", "prettier", "typescript"],
"groupName": "dev tooling",
"groupSlug": "dev-tooling"
}
],
"vulnerabilityAlerts": {
"enabled": true,
"schedule": ["at any time"],
"prPriority": 10
},
"packageRules": [
{
"matchUpdateTypes": ["major"],
"enabled": false
}
]
}
The key decisions here: security alerts bypass the weekend schedule and get PRs immediately. Major version updates are disabled entirely so they do not auto-open PRs; you handle those manually. Dev tooling is grouped into a single PR. Everything else batches to weekends with a five-PR cap.
Dependabot supports similar configuration in .github/dependabot.yml. The syntax differs but the principles are the same: schedule batching, separate schedule for security alerts, group dev dependencies.
Setting an SLA policy that teams will actually follow
Policy documents that say "update critical CVEs within 72 hours" are useless without enforcement. The ones that work are short, have clear ownership, and are referenced in your incident review process so you can see when the policy was violated.
A working policy looks like this:
Critical severity (CVSS 9.0+): patch within 24 hours or deploy a mitigating control (WAF rule, feature flag disable) and document why patching was deferred. An on-call engineer owns this.
High severity (CVSS 7.0-8.9): patch within 7 days. Engineering lead owns this, it goes into the current sprint.
Medium severity (CVSS 4.0-6.9): address in the next planned dependency update window, maximum 30 days.
The SLA only works if npm audit runs in CI and fails the build on new critical or high findings. Otherwise the policy has no teeth. Add this to your pipeline:
npm audit --audit-level=high
This exits non-zero on high or critical findings. It will not catch everything, but it will stop you from shipping code with known critical vulnerabilities in fresh dependencies.
Dependency hygiene is one of those things that feels boring until it is not. The teams that get burned are the ones who conflated "this hasn't caused a problem yet" with "this isn't a problem." Seaworthy flags outdated dependencies and known-vulnerable packages as part of its static analysis pass.
This article was generated by AI and summarises publicly available sources.
Seaworthy scans your repo for the issues covered in articles like this one.
Security gaps, exposed secrets, and misconfigurations — caught before you deploy. Free to run, no account needed.