← All posts
·6 min read·GuardLayer

Vulnerable npm dependencies: catch them before they ship, not after

npmDependenciesNext.jsSupply Chain

Your application code is maybe 5% of what you ship. The other 95% is node_modules — hundreds of packages, written by strangers, pulled in transitively, updated on someone else's schedule. So when people talk about "securing the app," they're usually auditing the 5% and ignoring the part that actually gets exploited. CWE-1395 — Dependency on Vulnerable Third-Party Component — is one of the most reliable ways into a modern web app, precisely because it doesn't show up in your diff.

The mechanism is boring, which is exactly why it works. You pin next at 14.1.0 six months ago. A few releases later, a Next.js advisory lands — say, an SSRF or cache-poisoning issue in the server runtime. The fix ships in 14.2.21. Your package.json still says 14.1.0. Nothing in your repo changed, no alarm fired, and the vulnerable version is now in production, serving traffic.

What "known-vulnerable" actually means

There's an important distinction here. This isn't a malicious package or a zero-day nobody has seen. A known-vulnerable dependency is a package where:

  1. A security researcher found a flaw,
  2. A CVE / advisory was published, and
  3. A patched version exists — but you haven't upgraded to it.

That third point is what makes it negligence rather than bad luck. The fix is sitting on the npm registry. Anyone reading the GitHub Advisory Database can see exactly which versions are affected, and bots do exactly that — they scrape public dependency manifests and lockfiles looking for ranges that resolve to a vulnerable version. A pinned old version is a published invitation.

Here's a package.json that looks completely ordinary in review:

{
  "name": "my-next-app",
  "version": "0.1.0",
  "dependencies": {
    "next": "14.1.0",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "axios": "^1.6.7",
    "lodash": "4.17.20"
  }
}

Three of those lines are live advisories. next@14.1.0 predates the SSRF/cache-poisoning fixes in 14.2.21. axios@^1.6.7 resolves below the 1.8.0 that closes an SSRF / credential-leak issue. lodash@4.17.20 is one patch short of the 4.17.21 that ships its last security fixes (ReDoS and prototype-pollution hardening). The other two lines — react and react-dom — are clean. Nothing here looks wrong, and that's the whole problem.

guardlayer scan · package.jsonLive engine output
Passed with warnings
76/100 · B
  • Warningpackage.json:5

    Dependency with a known advisory

    Upgrade to the patched version and run npm audit to confirm the advisory is resolved.
  • Warningpackage.json:8

    Dependency with a known advisory

    Upgrade to the patched version and run npm audit to confirm the advisory is resolved.
  • Warningpackage.json:9

    Dependency with a known advisory

    Upgrade to the patched version and run npm audit to confirm the advisory is resolved.

Why this slips past everyone

Three structural reasons, and all of them are about time, not skill:

  • The vulnerability arrives after the review. The code that imported next was reviewed and approved when 14.1.0 was current and clean. The advisory landed months later. There is no PR to re-review, no diff to catch it in. The risk appears in a file nobody is looking at anymore.
  • Transitive depth hides it. lodash might not even be in your direct dependencies — it could be three levels down, dragged in by a build tool. You never typed it, so you never think about it.
  • npm audit is opt-in and noisy. It only tells you anything if someone runs it, and when it does run it buries the two genuinely-exploitable advisories under forty dev-only warnings about packages that never touch production. People learn to ignore the wall of yellow, which trains them to ignore the red.

The result: the gap between "a fix exists" and "we shipped the fix" gets measured in months. Attackers measure it in hours.

The fix

1. Upgrade the flagged packages to a patched version.

npm install next@latest axios@latest lodash@latest
npm audit            # confirm the advisories are gone

Pin to a known-good floor instead of a stale exact version. For the example above:

{
  "dependencies": {
    "next": "^14.2.21",
    "axios": "^1.8.0",
    "lodash": "^4.17.21"
  }
}

Caret ranges let patch and minor security releases in automatically, while your committed lockfile keeps builds reproducible. Commit package-lock.json so everyone — and CI — resolves the exact same tree.

2. Make the audit a gate, not a suggestion. Run it in CI and fail the build on real severity:

npm audit --audit-level=high

This blocks a merge that introduces or keeps a high/critical advisory, without drowning you in low-severity dev-dependency noise.

3. Automate the upgrade PRs. Turn on Dependabot or Renovate so the upgrade arrives as a small, reviewable PR the day the advisory is published — instead of sitting in a manifest nobody opens until something breaks.

4. Scan the manifest on every push. npm audit is a point-in-time local command that's easy to skip. A scan wired into your push — like GuardLayer — checks package.json against the advisory list every time, with no one having to remember.

30-second self-check

# 1. What does the audit actually say? (high+ only)
npm audit --audit-level=high

# 2. Is a flagged package reachable in prod, or dev-only?
npm ls next axios lodash

# 3. Are you pinned to a stale exact version anywhere?
grep -nE '"(next|axios|lodash|jsonwebtoken|ws|semver|minimist)":' package.json

If npm audit reports anything at high or critical and the package is in dependencies (not just devDependencies), treat it as a production exposure and patch it before your next deploy. Or scan the whole manifest at once — GuardLayer flags known-vulnerable dependencies (and 19 other Next.js + Supabase issues) on every push and shows the patched version inline.

FAQ

Is a vulnerable devDependency as dangerous as a vulnerable dependency? Usually no. A dev-only package (a test runner, a bundler plugin) doesn't run in production, so its blast radius is your CI/build environment rather than your live app. It's still worth fixing — build pipelines get attacked too — but a vulnerable runtime dependency is the higher priority every time.

Does npm audit fix solve this on its own? Often, but not always. npm audit fix upgrades within your declared semver ranges; if the patched version is a major bump outside your range, it won't apply it without --force, and --force can break things. Treat it as a first pass, then handle the remaining majors deliberately.

Why flag a package that's only one patch version behind? Because that single patch is frequently the security fix itself — lodash@4.17.20 to 4.17.21 is exactly that kind of release, shipping the security hardening and nothing else. "Close to current" and "patched" are not the same thing.

The advisory needs a specific input to trigger. Am I really exploitable? Maybe not today. But you can't reason your way to "we never hit that code path" with confidence across a transitive tree you didn't write. The patched version costs you an npm install; the exploit costs you a disclosure email. Upgrade.

How is this different from a malicious or typosquatted package? This is a legitimate package with a published flaw and an available fix — the failure is simply not upgrading. Typosquatting is a different threat (a hostile package masquerading as a real one) and needs a different control: verify the package name and publisher before you install.

Catch this before it ships — free

GuardLayer scans every push for this and 19 other Next.js + Supabase issues, with the exact fix inline.

Keep reading