Everyone races to the newest CVE. I do the opposite — I open the patch that fixed the last one and ask one question: what did this fix not cover?
Most weeks, that question pays better than any 0-day hunt. A fresh bug makes you find the flaw, the reachable sink, and a working primitive — all cold. An incomplete fix hands you most of that for free. The vendor already told you where the danger lives (the diff), why it's dangerous (the advisory), and what input reaches it (the regression test). All that's left is the part they got wrong.
And patches get rushed — written under disclosure pressure, scoped to the one PoC in the report, shipped before anyone audits the class. That gap is where I live.
Why patches come up short
After enough of these, the failure modes rhyme:
- Patched the payload, not the primitive. The fix blocks the exact string from the report and leaves the capability intact. Denylists age badly — the language surface is always bigger than the list.
- Fixed one entry point, missed the siblings. The guarded function is called from three places; the patch lands on the one in the report. The other two still sail past.
- Order of operations. The check is added, but after the side effect — a cleanup that runs before the authz check, an env var expanded before the path is validated.
CVE-2026-4270in aws-api-mcp-server is a clean public example: the fix had to move workdir validation after env expansion. - Spec-compliant by accident. Sometimes the “fix” just documents the behaviour as by-design. Know this so you don't burn a week — spec-compliant behaviour is not a vuln, and good programs close it N/A.
The method
The loop, end to end:
- Pull the fixing commit, not the advisory.
git show <fix-sha>. The advisory is marketing; the diff is the truth. Read every line it touched — and the lines right next to them. - Map the guard to its callers. New check at
foo()? grep every call site, every sibling doing the same dangerous thing, every public route that reaches it. The patch guards one path; you want the graph. - Re-derive the primitive, ignore the payload. If they blocked a string, ask what capability it was using and whether the capability survives. SSTI sandboxes teach this best: a denylist sandbox that blocks a few methods still loses if method or property access itself isn't gated, or if tests, filters, and string-callables go unchecked. Template engines are a forever-class — even this week
CVE-2026-45017(Python Liquid's FileSystemLoader) is another “the loader trusted the path” traversal. - Prove reachability, then stop. Can an attacker hit the unguarded path right now, with realistic privileges? If yes, you have a finding. If it needs a contrived precondition, write it down and move on. The 5-minute rule has saved me more bounties than stubbornness ever did.
- Sweep the class, then walk away. Once you find one gap, audit every sibling in that module in one pass. Either you find a cluster, or you confirm the maintainer internalised the class. Both are worth knowing; the second tells you to stop re-auditing that repo forever.
A checklist you can steal
- Read the fixing diff, not the advisory.
- List every caller of the newly-guarded function.
- Identify the primitive the original payload used — does it survive?
- Check order: does the guard run before or after the side effect?
- Confirm reachability with realistic privileges (exploitable now).
- Sweep siblings in the same module; record cluster-or-clean.
- Kill it fast if it's spec-compliant or needs a contrived precondition.
Why it's worth it
Incomplete-fix findings are novel (a real CVE/GHSA, not a dup), fast (the vendor did your recon), and defensible (you point at the exact line the patch missed). The catch is the embargo: you're working on something not yet public, so disclosure discipline matters. Report through the vendor's channel, sit on the details until it's fixed and public, and only then write the war story.
Sita walked into fire to prove what was already true. A patch is the opposite — it claims something is true before it's been tested. My job is the test. The maps are public. Go read a patch.
No comments yet