A critical vulnerability lands in a dependency you use. There is no fixed version. The maintainer has not responded in four months, the package has not seen a commit in two years, and it sits three levels deep in a transitive tree you do not control.
Every process you have assumes a patch exists. This post is what to do when one does not. For whoever owns the remediation backlog and has to write something defensible in the risk register.
Establish what you are actually exposed to
Before choosing a response, narrow the problem. Most unpatchable findings shrink considerably under three questions.
Is the vulnerable code reachable from your entry points? A vulnerability in a function your application never calls is not exploitable through your application. This is the single largest reduction available, and it needs recording rather than assuming, because a refactor can make it reachable and nothing will tell you.
Can the triggering input reach it? Many vulnerabilities need attacker-controlled input in a specific shape. If the only caller passes a constant, or a value from a trusted internal source, the path is closed by data flow rather than by patching.
What is the actual impact if it fires? Denial of service in a batch job that restarts is not the same finding as remote code execution in your public API, even at identical CVSS scores. Severity from an advisory is context-free by construction. Yours is not.
Write the answers down at the moment you determine them, with the date and the code version they were true for. That record is what makes the decision defensible six months later, and it is the part people skip.
The options, most to least preferable
Upgrade around it. Often the vulnerable package is pulled in by something else that has a newer release depending on a fixed version. Check the parent before concluding you are stuck. Most transitive findings resolve this way and it takes ten minutes to check.
Override the resolved version. Every ecosystem has a mechanism: npm overrides, Yarn resolutions, Maven dependencyManagement, Gradle resolutionStrategy, Python constraints files. You force the fixed version even though the intermediate package asked for an older one.
{ "overrides": { "vulnerable-lib": "2.4.1" } }
This works when the fixed version is API-compatible and is the fastest real fix available. Test properly: you have overridden a constraint the intermediate package declared, and it declared it for a reason.
Patch it yourself, in place. patch-package for npm, a wheel rebuild for Python, a shaded jar for the JVM. You carry a small diff applied at install time.
Do this when the fix is small and understood. Two obligations come with it: the patch must be reviewed like any other security change, and it needs an owner who re-checks upstream periodically, or you will carry it for years after a real fix ships.
Fork and maintain. For an abandoned package you depend on heavily. Real cost, real control, and worth it only when the alternative is worse.
Remove the dependency. Frequently underrated. A package used for one function, that you could write in forty lines, is a candidate for deletion rather than remediation. Every removed dependency is a permanent reduction rather than a one-time fix.
Compensating controls. When you cannot change the code at all.
What a compensating control actually is
The phrase gets used loosely to mean "we did something adjacent". A control that counts has three properties: it addresses the same risk, it is enforced rather than advisory, and it is verifiable.
Real examples, by vulnerability shape:
- Deserialisation flaw: stop the untrusted input reaching it. A schema check at the boundary, or an allowlist of accepted types.
- Path traversal: canonicalise and validate at the entry point, before the vulnerable library sees the value.
- Remote code execution in a parser: run the parsing in an isolated process with no network egress and a read-only filesystem, so the consequence is contained even if it fires.
- Denial of service: rate limiting and a request size cap in front of the component.
- Vulnerable admin endpoint: network policy so it is unreachable from outside the cluster.
The test for whether yours counts: can you demonstrate it works. Send the malicious input and show it does not arrive. A control you have not tested is an intention, and an auditor will ask, correctly, how you verified it.
Virtual patching at a WAF is a legitimate member of this list and the weakest one. It is signature-based, bypassable, and it belongs as a layer rather than as the answer, but as a same-day measure while you do something better, it is worth having.
Accept the risk properly, or not at all
Sometimes the honest answer is that the exposure is low, the fix is expensive, and you will live with it. That is a legitimate engineering decision and it becomes a problem only when it is made silently.
A real risk acceptance has: what the finding is, why it is not being fixed now, what compensating controls exist, who accepted it, and when it expires. The expiry is the load-bearing field. Without it, an acceptance becomes permanent by default, and nobody ever revisits a decision that does not come back around.
Keep them somewhere reviewable rather than in a ticket comment. A suppression file in the repository works well, because it sits next to the code and shows up in review when that code changes:
- id: CVE-2024-XXXXX
package: abandoned-parser
reason: "Not reachable: only called from the import CLI, not the service.
Input is operator-supplied. Re-check when import moves to the API."
compensating: "Import runs in a sandbox with no egress."
accepted_by: platform-lead
expires: 2027-01-15
Push upstream while you do the rest
None of the above fixes the problem for anyone else. If the package is alive, open an issue with a reproduction. If you developed a patch, offer it. If it is genuinely abandoned, say so publicly, because the next team hitting this deserves to find your finding rather than repeating your week.
This is slow and it is the only path that reduces the total amount of this work in the world.
The concession
Everything here is more expensive than npm update, and some of it is judgement dressed as process. Two competent engineers can look at the same unreachable-vulnerability analysis and disagree about whether it is truly unreachable, and reachability analysis has known blind spots: reflection, configuration-driven dispatch, and entry points nobody enumerated.
So treat the analysis as an ordering rather than a proof, keep the expiry short when the argument rests on reachability, and re-run it when the code changes. The cases where this reasoning fails are the cases where somebody assumed once and never looked again.
The implication
A backlog process that only knows how to apply patches will silently stall on every finding without one, and those accumulate at the bottom of the list where nobody reads.
The fix is to make "no patch available" a state with its own workflow, rather than an exception. Decide, document, control, expire. Then the unpatchable findings are managed rather than merely old.