Lukas' Notes

A filter that rejects the literal string 127.0.0.1 feels airtight. It is not. The string http://127.0.0.1 names exactly one host, but that host has dozens of spellings, and a server-side request forgery defence that matches strings rather than resolving hosts will fall to almost all of them.

The first failure mode is alternative encodings of the same address. The loopback_IPV4 address 127.0.0.1 can be written as a shortened dotted form (127.1), as a decimal number (2130706433), or as a hex number (0x7F000001). They all resolve to the same destination, but a string filter has to enumerate every one of them.

The second failure mode is letting the host mean something else. A domain the attacker controls can always be pointed at 127.0.0.1, so http://wut.fbi.com is a perfectly normal-looking URL that resolves to the attacker’s chosen internal target. Credentials can be embedded in the authority: https://example.com:pw@evil.com looks like example.com at a glance but the host part is evil.com. An open redirect on a trusted domain (https://google.com/view?adurl=https://evil.com) wraps an off-limits destination inside an approved one. Some HTTP clients even expand brace syntax (https://{example,evil}.com) and apply Unicode normalisation, so https://ℂᵤⓇℒ。𝐒🄴 reaches curl.se.

The deeper lesson is that a URL is not a string but a parsed structure whose components influence resolution in ways the surface text hides. The host is decided after scheme-splitting, authority-parsing, credential-stripping, IDNA normalisation, and DNS resolution, in that order, and a filter that inspects the raw bytes before any of these steps can be fooled at whichever step it skipped.

The workable defence therefore resolves the URL the way the client will resolve it, and only then checks the resolved host against a deny-list of internal ranges — never the other way round. You still need to do this on a system you trust, with redirects disabled, with the metadata ranges included, and ideally with a hardened fetch client that refuses schemes other than http(s) and pins redirects to first-party hosts. The point is not to enumerate spellings; the point is to compare hosts after resolution, where 127.1 and 2130706433 have already become 127.0.0.1.

The corrected mental model: a URL string is not what your filter sees, it is what your client connects to. Validate the endpoint the fetcher will actually reach, not the text the user typed.