SSRF: checking the URL is not the same as checking the IP
Any feature that fetches a URL on behalf of a user (webhooks, link previews, image imports, PDF renderers) can be pointed at addresses only your servers can reach. The classic target is the cloud metadata endpoint at 169.254.169.254, which can hand out temporary credentials.
String checks lose
A filter that rejects URLs containing localhost or 127.0.0.1 misses many spellings of the same address. We resolved each of these with getaddrinfo on Linux and all four came back as 127.0.0.1:
2130706433(the address as one decimal number)0x7f.1(hexadecimal with the short form)127.1(short form)0177.0.0.1(octal)
Python's ipaddress module rejects some of those strings, so a validator that only accepts what ipaddress can parse will let them through and then hand them to a socket library that understands them. The IPv6 form ::ffff:127.0.0.1 is another spelling of the loopback address, and ipaddress does flag it as loopback.
Check the resolved address
Resolve the name yourself, look at every address it returns, and reject anything that is not a public one:

is_global is false for loopback, private ranges, link-local addresses (including the metadata address) and unique-local IPv6. Check all returned records, not the first one, because a name can have several.
Three ways it still fails
- DNS rebinding. The name resolves to a public IP when you check it and to an internal one when the HTTP client connects a moment later. The defence is to connect to the address you checked, not to resolve the name a second time. Many HTTP libraries let you supply a custom resolver or transport for this.
- Redirects. A public URL can answer
302tohttp://169.254.169.254/. Turn off automatic redirects, or run the same check on every hop. - Other schemes and ports. Allow only
httpandhttps, and think about restricting ports. Services like Redis, which ignore garbage lines and keep parsing, have been attacked through HTTP requests aimed at their port.
Reduce what a successful request can reach
Network controls are more reliable than code: run fetchers in a segment with no route to internal services, and use an egress proxy with an allowlist. On AWS, requiring IMDSv2 means the metadata service needs a session token obtained with a PUT request, which most simple SSRF primitives cannot send, and a low hop limit stops containers from reaching it. Whatever your cloud, remove the credentials you do not need from the instance role.
More than one cloud has a metadata endpoint at that address
169.254.169.254 is not an AWS-specific address; it is the conventional link-local metadata address used by several major cloud providers, including Google Cloud and Azure, each with their own API on top of it. Google Cloud's metadata server additionally requires a Metadata-Flavor: Google header on requests, which blocks the simplest SSRF payloads (a bare URL fetch with no custom headers) while doing nothing against a fetcher that lets the attacker control request headers too. Azure's Instance Metadata Service similarly requires a Metadata: true header. Treat the header requirement as a minor speed bump, not a defence, and keep the network-level and address-validation controls as the real protection regardless of which cloud you are on.
It is also worth checking the whole address block, not just the one IP: 169.254.0.0/16 is the entire link-local range, and 127.0.0.0/8, not only 127.0.0.1, is all loopback. A filter that special-cases the two most common literal addresses and validates everything else with string matching will miss 127.0.0.2 or another address inside either block, which is one more reason to reject by parsed, resolved address and range membership (is_global, or an explicit check against the relevant private and link-local networks) rather than by comparing strings.