SSRF: the vulnerability hiding in your URL-fetching endpoint

Updated 2026-09-25 · FreeTools

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:

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

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.

Test the CORS proxyCheck response headers