The check that cannot hold
Northwind Commodities let customers register a webhook URL for trade confirmations. The validation was careful by the standards of most code: parse the URL, reject non-HTTPS, resolve the hostname, reject private and loopback ranges, reject link-local. Then hand the URL to the HTTP client.
That last sentence is the vulnerability. Between the resolution used for validation and the resolution used for the request, the attacker controls DNS. A record with a 1-second TTL returns a public address to the validator and 169.254.169.254 to the client. This is a time-of-check-to-time-of-use race, and the attacker does not have to win it by luck — they can serve alternating answers until it lands. Our tester got it on the third attempt.
Redirects are the same class of hole. A validated public URL returning 302 to an internal address is followed by default by nearly every HTTP client, and the redirect target is typically not re-validated at all.
Validate the connection, not the string
The correct control sits at the socket. Use a custom dialer that resolves the host once, checks the resulting IP against the deny list, and connects to that exact address while preserving the Host header and SNI. There is then no second resolution to poison. The same dialer is invoked for every hop of a redirect chain, so the check applies uniformly, and the client is configured with a redirect cap of 3.
The deny list is not just RFC 1918. It needs loopback, link-local (169.254.0.0/16 and fe80::/10), IPv4-mapped IPv6, unique local addresses (fc00::/7), carrier-grade NAT (100.64.0.0/10), and 0.0.0.0. Decimal and octal IP encodings must be normalised before comparison, because parsers disagree about them and attackers know which ones.
Where the platform allows it, the stronger version is architectural: the pod has no default route to the internet, and all outbound traffic goes through a forward proxy with an allowlist. Then a bug in the dialer is not exploitable, because there is no path. Egress network policy is the control that survives your own mistakes.
Why the network fix is not enough
Suppose the attacker reaches an internal service anyway — through a different bug, a compromised dependency, or a service that legitimately needs broad egress. What happens next is determined entirely by whether internal services authenticate their callers.
At Northwind, several did not. The pattern was an X-Internal-Request: true header, trusted because 'only our services can reach this'. A header is not a credential; it is a wish. Any SSRF that can set headers, and many can via URL parsing quirks or protocol smuggling, defeats it instantly.
The replacement is workload identity: mTLS between services with SPIFFE identities issued by the mesh, and authorisation policies naming the permitted caller identity per route. An SSRF-forged request originates from the vulnerable service's own socket, so it does carry that service's identity — which is precisely why the policies must be least-privilege per route rather than 'any mesh workload'. The webhook dispatcher is authorised to reach nothing internal at all.
Cloud metadata and detection
The metadata endpoint deserves specific handling because it converts SSRF into credential theft. On AWS, enforce IMDSv2 with HttpTokens required and HttpPutResponseHopLimit set to 1, so a request originating in a container cannot reach it. On GCP require the Metadata-Flavor header, which most SSRF primitives cannot set. Confirm by testing from inside a running pod rather than reading the Terraform.
Detection is easier than most teams assume once egress is proxied. Alert on denied destinations by source service, and on new destination cardinality — a service that has contacted the same four hosts for six months and suddenly resolves a fifth is worth a look. Northwind's proxy logs about 30 denies a day, almost all misconfiguration, which keeps the signal legible.
The review question we now ask of any feature that fetches a URL supplied by a user is not 'is the URL validated'. It is: if this fetch reaches an arbitrary internal address, what does the receiving service do with an unauthenticated request. If the answer is 'serve it', the SSRF fix is in the wrong service.

