SSRF: the vulnerability hiding in your URL-fetching endpoint
In short: SSRF is what happens when your server fetches a URL
a user chose. If you pass that URL to fetch() unchecked, the attacker asks your
server for http://169.254.169.254/ and receives the result. The fix is to resolve
the hostname yourself and reject private and link-local addresses.
Server-Side Request Forgery sounds exotic but it is the most common serious flaw in any API that accepts a URL. It matters even more now, because cloud instance metadata endpoints hand out credentials that let an attacker move sideways into your whole account.
What the attacker actually gets
An endpoint that fetches a user-supplied URL, such as a CORS proxy, a link previewer or a webhook tester, can be pointed at:
- Cloud metadata —
169.254.169.254returns IAM credentials on AWS and similar credentials on other clouds. - Internal services — databases, admin panels and dashboards that are not exposed to the internet but are reachable from your server.
- Localhost — services bound to
127.0.0.1that assume they are unreachable from outside. - Private ranges — the whole RFC 1918 space, plus IPv6 equivalents.
Why a blocklist on the hostname is not enough
The naive guard checks the string you were given. That fails in three ways:
# 1. the name looks public...
http://internal-service.corp/
# 2. ...but resolves to a private address
http://foo.example.com.127.0.0.1.nip.io/
# 3. it looks private but the redirect goes public then back in
https://evil.example/ -> http://169.254.169.254/
The fix is to resolve the hostname, inspect the resulting IP addresses, and only then connect. And you must repeat the check after every redirect, because the destination of hop two is entirely under the remote server's control.
The resolution step
import socket, ipaddress
def is_public(host):
try:
infos = socket.getaddrinfo(host, None)
except socket.gaierror:
return False
for info in infos:
ip = ipaddress.ip_address(info[4][0])
if (ip.is_private or ip.is_loopback or ip.is_link_local
or ip.is_multicast or ip.is_reserved or ip.is_unspecified):
return False
return True
if not is_public(user_url_hostname):
raise ValueError("blocked: internal address")
Note that this returns false if any resolved address is private. A hostname with a mix of public and private records is a classic bypass, and a checker that only inspects the first address will let it through.
Re-check on every redirect
Disable automatic redirects and follow them manually, validating each hop:
client = build_client(redirect=Policy.none())
for _ in range(5):
assert is_public(current_host) # every hop, not just the first
resp = client.get(current_url)
if not resp.is_redirect:
break
current_url = urljoin(current_url, resp.headers["Location"])
Other limits worth setting
- Response size cap — stop streaming at a few megabytes.
- Timeout — a slow internal port should not hold a worker open.
- Port allowlist — if you only fetch HTTPS on 443, say so.
- No internal hostnames — reject anything ending in
.internalor.localbefore resolving.
How FreeTools handles it
The FreeTools proxy resolves every hostname, rejects loopback, link-local, private and cloud metadata ranges, follows at most five redirects, and re-validates the destination on every hop. The same guard covers the header checker, so you can test that pattern without exposing your own server.
Frequently asked questions
What is server-side request forgery?
SSRF is when an attacker controls a URL your server fetches, using your server's network position to reach systems it cannot access directly. It commonly targets cloud metadata endpoints at 169.254.169.254 to steal instance credentials.
How do I prevent SSRF in a URL-fetching API?
Resolve the hostname yourself, reject the request if any resolved IP is loopback, link-local, private, multicast or reserved, and repeat that check after every redirect. Also set a timeout, a response size cap and a port allowlist.
Why is checking the hostname string not enough?
Hostnames can resolve to private addresses through public DNS services, decimal or obfuscated IP notation can bypass naive string matching, and a public URL can redirect to an internal one. Only inspecting the resolved IP addresses is reliable.
Do open CORS proxies allow SSRF?
A naive one does, which is why open proxies are among the most abused server misconfigurations. A safe proxy resolves and validates the target, and re-validates on every redirect hop, as the FreeTools proxy does.