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).
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
1. Scan: turn the state into numbers
First, see what's where, by machine. Manual ad-hoc checks always lapse.
Auto-discover lockfiles and scan
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.Know that severity isn't one number
Exclude noise WITH a recorded reason, not by ignoring
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.
Don't commit on the production working tree
Standardize on one direction: local→push→deploy
Don't trust HTTP 200 as 'healthy'
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.
Orphaned/EOL code: isolate BEFORE deleting
Confirm it's truly unreferenced first
Explicitly hand off what you can't fix
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).
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.
Read next
- Dependency monitoring: installing and using osv-scanner · not falling behind on CVEs
- Incident: a neglected public RCE billed for fraud
- Operations: production receives only / self-hosted Git vs GitHub
- Foundation: the security baseline checklist
FAQ
QWhen is vulnerability work actually 'done'?
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?
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?
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.