📅 Day 183 — The Fix Had Two More Holes: A Live DNS-Rebinding Bypass Hunt
🔄 Topic
An independent security review of yesterday’s SSRF fix — Chromium IP-pinning meant to close DNS-rebinding — found two further bypasses, both confirmed live against a real browser and a real local server, before either was patched.
🎯 Goal
Prove or disprove the IP-pinning fix under real adversarial conditions instead of trusting that “the tests pass” means “the hole is closed.”
🛠 What I Did
I ran an independent acceptance review against my own fix and refused to accept it on faith.
Main areas covered:
- Defect 2: the pinning fix trusted the original requested hostname’s address without re-validating it, reasoning an earlier gate had already checked it — but that gate checked the hostname string, not what a DNS lookup milliseconds later would resolve to. A rebinding attacker could pass the first check and pin a forbidden address for the second. Confirmed live: Chromium launched with the forbidden address pinned, no alarm raised, just a quiet navigation timeout.
- Defect 3: redirect-following trusted the very first hop by hostname, assuming the DNS-safety wrapper always ran — except Node’s HTTP client never invokes a custom DNS lookup at all when the URL is already a literal IP address. Confirmed live: fetching
http://127.0.0.1:<port>returned a real local server’s full response with zero SSRF checks. - fixed both by validating every hop identically, including hop zero and including the original hostname’s fresh resolution — no more trusted exceptions
- extended the test fixtures with a reusable loopback network-guard mock so the regression tests exercise real HTTP/redirect/timeout mechanics without depending on the now-closed bypass
- verified IPv6 pinning against a real production IPv6-only site end-to-end, closing a limitation that had only ever been unit-tested
- confirmed TLS certificate validation stayed intact throughout — expired, wrong-host, and self-signed certificates were all still correctly rejected
🔗 Key Cybersecurity Connections
This is the difference between a fix and a proof. DNS rebinding exists precisely to exploit the gap between “we checked this hostname” and “this is what the hostname resolves to right now” — and my own fix reintroduced that exact gap in a new place while closing it in the old one. Independent review with live reproduction, not test-suite trust, is what caught it.
The IP-literal gap is the other classic SSRF lesson: safety code that only runs during DNS resolution does nothing for a URL that never needed to resolve anything.
🔍 Investigation Questions
- Does every hostname get revalidated at the moment of connection, including the first one?
- What happens when the target URL is already a literal IP address?
- Can a “trusted because checked earlier” shortcut hide a time-of-check/time-of-use gap?
- Are all private ranges covered, including the ones people forget — like the cloud metadata range?
- Did the fix get proven against a live target, or only against a mock?
🚨 Detection Opportunities
Checks for outbound-fetch hardening:
- any code path with a “trust this hostname, it was checked earlier” comment
- DNS-safety wrapper skipped because the URL was already an IP literal
- redirect hop zero treated differently from discovered hops
- private ranges list missing 169.254.0.0/16 or other commonly forgotten space
- a security fix merged without a live, adversarial reproduction attempt
Example:
project=browser-research-adapter
signal=trusted_hostname_shortcut_in_ssrf_guard
risk_area=time_of_check_time_of_use_gap
triage=reproduce_live_before_and_after_the_fix
🧭 MITRE ATT&CK Techniques
Possible mappings for the vulnerability class:
- T1090 — Proxy
- T1557 — Adversary-in-the-Middle (DNS rebinding is a variant of this pattern)
🗺 Visual Investigation Diagram
"Fixed" IP-pinning shipped
↓
Independent review: assume it's wrong, try to break it
↓
Defect 2: original hostname trusted, not re-validated
Defect 3: IP-literal URLs skip DNS-safety entirely
↓
Both reproduced live before any patch
↓
Every hop validated identically, no exceptions
↓
IPv6 + TLS verified live, not just unit-tested
⚠ Challenges
The hardest part was psychological: yesterday’s fix felt done. Finding two more holes in it the next day is uncomfortable, and it is also exactly what an independent review is for. A review that only checks “did the tests pass” would have missed both.
📚 What I Learned
I learned that SSRF fixes have to be re-attacked, not just re-tested. Both defects survived a full unit-test suite because the tests encoded the same trust assumption the code did. Only live reproduction against a real server exposed the gap.
➡ Next Steps
- Treat every “trusted because checked earlier” comment as a review target
- Keep the loopback-fixture pattern for future network-guard changes
- Re-attack this guard again after any redirect or DNS-handling change
- Apply the same live-reproduction discipline to the next security fix
🧠 Reflection
Two bypasses found the day after “fixed” is not a failure of yesterday’s work — it is what happens when review is adversarial instead of confirmatory. I would rather find these than have someone else find them first.
🧩 Lessons Learned
What worked
Independent review with a genuine attempt to break the fix, reproduced live before patching.
What broke
A trusted-hostname shortcut and a DNS-lookup assumption that IP-literal URLs violated.
Why it broke
Both defects hid inside reasonable-sounding assumptions that nobody had tested against a real adversary.
Fix / takeaway
Validate every hop identically, with no trusted exceptions, and prove fixes live — a passing test suite is not the same as a closed hole.
📈 Skill Progression Context
This supports my cybersecurity progression because DNS rebinding, time-of-check/time-of-use gaps, and adversarial self-review are exactly the reasoning real appsec and red-team work depends on.
😄 TL;DR
Reviewed my own SSRF fix like an attacker and found two more ways through it — both closed, both proven live.
