← Back to Blog18 October 2026 · Kieran

    The IPv6 Bug That Could Have Undone Our Own Security Fix

    We fixed a real SSRF bug, then found our own fix had a gap -- because of how browsers and servers silently rewrite IPv6 addresses.


    We'd just closed a set of SSRF (Server-Side Request Forgery) gaps in our site-audit engine, the class of bug covered in our previous post. Job done, tests passing, deployed. Then, going back over the same fix with fresh eyes a few hours later, we found the fix itself had a hole in it.

    The setup

    Part of the fix blocks any fetch from reaching a private IP address, such as 127.0.0.1, 169.254.169.254, 192.168.x.x, and their IPv6 equivalents. IPv6 has a notation for embedding an IPv4 address inside it: ::ffff:127.0.0.1 means exactly the same destination as 127.0.0.1. Our first fix checked for that, using a pattern that looked for the dotted-decimal form.

    The gap

    Here's the part that isn't obvious until you test it directly: when a URL like https://[::ffff:127.0.0.1]/ gets parsed by a standard URL parser (the same one every browser and server uses), it gets silently rewritten into hex form: [::ffff:7f00:1]. Same address. Completely different string. Our check, which was looking for the dotted form, never saw it.

    We confirmed this directly rather than assuming it: parsed the same address both ways, checked what a real request actually produces, and watched our own guard wave the "converted" version straight through.

    The fix

    Instead of pattern-matching the text of the address, we now convert the address to its actual numeric value first, then check that number against the private ranges, regardless of which of the (several) equivalent ways it was written. Same approach for a second, older notation (::127.0.0.1, without the ffff) that has exactly the same problem.

    The lesson, not just the fix

    The interesting part isn't the specific bug. It's that a security fix reviewed by reading the code looked correct. It only broke when we ran the actual address through the actual parser and checked what came out the other side. Code review catches what you think to look for. Running the real thing catches what you didn't.

    We don't consider a security fix finished until we've tried to break it, not just written it. This one took a second attempt to get right, which is exactly why we did a second attempt.