Skip to content
>_ITDITDWeb Security Platform

By Stack

Fixing dependency CVEs for real: scan, fix, isolate, and keep watching

A practical playbook for actually fixing dependency vulnerabilities (CVEs) in a small fleet, built on a 4-part definition of done — scan, fix, isolate/hand off, monitor — with field lessons: change-detection to beat alert fatigue, don't trust HTTP 200, and don't let fixes vanish (local→push→deploy).

Published 2026-06-11 Updated 2026-06-11 8 min read

For: anyone running several web apps (mixed frameworks) with a small team, who wants to fix dependency vulnerabilities (CVEs) for real, once. No attack steps — only the practice of fixing, making fixes stick, and keeping watch. For the kind of incident that often kicks this off, see a neglected public RCE billed for fraud.

This site's view: small teams stay safe with two disciplines

What works on a small team isn't flashy tooling — it's two disciplines: 1) automated change-detection (alert only on new vulnerabilities) and 2) local→push→deploy (production receives, never edits). This site runs the same way: a dependency audit (pnpm audit) before every deploy plus a daily cron, with push-to-deploy delivering to production. Treating security as "a system that keeps firing," not "a one-time fix," is the cheapest thing that lasts.

Fix the 4-part "definition of done" first

A guardrail against calling things "done" prematurely. Decide up front: until all four line up, it's incomplete.

1. Scan

put the state into numbers

2. Fix

root-fix the non-dev crit/high

3. Isolate/hand off

make the un-fixable explicit

4. Monitor

put daily change-detection in

Vulnerability work done = a set of four. Miss one and a hole stays under 'I fixed it.'

1. Scan: turn the state into numbers

First, see what's where, by machine. Manual ad-hoc checks always lapse.

1

Auto-discover lockfiles and scan

Find composer.lock / package-lock.json / pnpm-lock.yaml and run them daily through an open-source scanner (osv-scanner or pnpm audit). It only reads lockfiles, so the load is tiny.
2

Know that severity isn't one number

The same flaw can be High on CVSS (say 7.5) but Moderate on a vendor's vetted rating. CVSS is machine-computed and tends to over-count. Decide which basis sets your threshold, and make sure the whole team knows which one is in use (→ what CVSS is). Cross it with whether it's actually exploited (KEV) to prioritize.
3

Exclude noise WITH a recorded reason, not by ignoring

Test/build-only dev dependencies often aren't in the production bundle and may carry no real risk. Use the scanner's grouping to drop dev from notifications, but record why you excluded it. Stay able to answer "isn't this still unfixed?" later.

2. Fix: root-fix, not symptom-hide

First aid (symptom-hiding) and a root fix are different jobs. Do both to finish.

Symptom-hiding only (containment)

  • block only "suspicious-looking" requests at a reverse proxy
  • the symptom stopped, so call it "handled"
  • the vulnerable dependency stays — the RCE lives on

Root fix (correct)

  • upgrade the vulnerable dependency to the patched version
  • after upgrading, confirm the signature is gone in your logs
  • treat first aid and the root fix as two separate jobs, do both

Build-verify major bumps BEFORE production

Unlike a patch, a major-version jump (framework 14→15, a UI kit 4→6, etc.) carries breaking changes. Bumping blindly and breaking the prod build is the worst case. Put the edited source in a prod-identical runtime (same Node/PHP version container) and get the build/type-check fully green before you push. Clear errors one by one (sync→async codemods, multi-line CSS class strings, a retired global type namespace, etc.). If the old container keeps running, a failure rolls back with zero downtime.

Completion includes the fix's durability

A perfect fix is worth zero if the next deploy erases it. This is the most common trap.

1

Don't commit on the production working tree

A direct commit on production forks history from the central repo, and the next deploy (pull / checkout -f) overwrites it and the fix vanishes. Production is "where deploy results land," not where you edit.
2

Standardize on one direction: local→push→deploy

Edit locally → commit → push → (production) pulls / auto-deploys. Even a change you had to verify on production must be rewound to local and re-pushed (→ making production receive-only).
3

Don't trust HTTP 200 as 'healthy'

On shared hosting, a fatal error can still return 200. Verify with real content — is the expected heading/text rendered (curl | grep), are there new errors in the logs, and walk the dynamic routes (DB-backed, parameterized), not just the homepage. Caches can delay the change, so wait or clear first.

3. Isolate/hand off: make the un-fixable explicit

You can't always fix everything now. The trick is to never leave "can't fix / not my area / unused" ambiguous.

1

Orphaned/EOL code: isolate BEFORE deleting

Old source you no longer serve (EOL frameworks, etc.) — don't delete outright; rename to isolate it and leave a marker noting when/why and where it used to serve. Drop it from scans too. "Delete" is irreversible; "isolate" is reversible and keeps the trail.
2

Confirm it's truly unreferenced first

Trace the reverse-proxy config, build setup, and mounts to confirm nothing references it before isolating — cut off the risk of accidental re-serving.
3

Explicitly hand off what you can't fix

For things outside your reach, hand them off with "who / what" stated, rather than leaving them. The value of an inventory is not letting un-fixed turn into invisible neglect.

4. Monitor: only with daily change-detection is it done

Now, finally, put the firing mechanism in. Without it, a fix you made won't tell you when it recurs.

Alert on what's NEW, not everything every day

Diff scan results against the previous run (a state file) and notify only when a new critical/high appears. The same content every day gets ignored (alert fatigue). Summarize into one email (new / resolved / current) on a daily cron. Even shared hosting handles one scanner binary + cron fine (the load is milliseconds; add nice for peace of mind).

diff
notify only on new = no fatigue
daily
keep it running on cron
1 email
new / resolved / current summary
all 4
done only once monitoring is on

The "secrets" and "keys" an inventory also surfaces

Fixing CVEs for real tends to surface non-dependency holes too. Two classics — a secret file left in a public directory (an old token sitting in the webroot; if it came from a shared template, every site has the same hole) and a root key handed to an environment that can be compromised. Both create the "one leak, everything" pattern, so check them in the same sweep (each deserves its own deep dive). For secrets basics, see storing secrets safely and the baseline checklist.

FAQ

QWhen is vulnerability work actually 'done'?
A

Only when four things line up: 1) you scanned and put the state into numbers, 2) you fixed the non-dev critical/high, 3) you explicitly isolated or handed off what you can't fix / orphaned code, and 4) you put daily change-detection (monitoring that catches new and recurring issues) in place. Until 4 is in place, it's not done — dependencies turn vulnerable again tomorrow.

QWon't daily scanning cause alert fatigue?
A

Alerting on everything daily gets ignored fast. Diff against the previous result (a state file) and notify only when a NEW critical/high appears — change-detection. Not sending the same thing every day is what makes monitoring actually last.

QWhy does a fix sometimes revert to vulnerable?
A

Committing directly on the production working tree means the next deploy (a pull or checkout -f) overwrites and erases the fix. Standardize on one direction — local→push→deploy — and make production 'receive only.' A perfect fix on a workflow that erases it is worth zero.