Security Guides

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

How to actually fix dependency CVEs in a small fleet: a 4-part definition of done (scan, fix, isolate, monitor), change detection, and keeping fixes deployed.

Published 2026-06-11 Updated 2026-10-04 8 min read
On this page

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. Setting up alerts that keep running, instead of treating it as a one-time fix, is the cheapest approach that lasts.

Decide the four conditions for "done" first

This keeps you from calling things "done" too early. Decide up front: until all four line up, it's incomplete.

1. Scan

put the state into numbers

2. Fix

fix the non-dev crit/high at the source

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 vulnerability remains even though you think you fixed it.

1. Scan: count what you have now

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: update the vulnerable version itself

A temporary measure that hides the symptom and a fix of the actual cause are different jobs. Do both to finish.

Symptom-hiding only (the cause remains)

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

Fixing the cause (correct)

  • upgrade the vulnerable dependency to the patched version
  • after upgrading, confirm the signature is gone in your logs
  • treat the temporary measure and the actual 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. Upgrading without checking and breaking the production build takes the service down. 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.

Check that the next deploy won't undo the fix

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

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: write down what you can't fix

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 — this prevents accidentally serving it again.
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: set up daily change detection

Now, finally, put the alerting 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

API keys and SSH keys an inventory also finds

Fixing CVEs for real tends to surface non-dependency problems too. Two common ones: 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 problem) 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.