SSRF: how one URL field reaches your cloud metadata

A feature that fetches a URL for the user can be turned around to fetch your internal network instead. Here is how server-side request forgery works, why cloud metadata is the usual target, and the defences that hold up.

A request leaving a web server and looping back into the internal cloud network

Plenty of features ask the server to fetch something on the user's behalf: link previews, "import from URL", webhooks, PDF generators that load images, avatar uploads by address. Each one is a small HTTP client running inside your network. Server-side request forgery (SSRF) is what happens when an attacker chooses where that client goes.

The basic attack

A link-preview endpoint takes a URL and returns the page title:

POST /api/preview
Content-Type: application/json

{"url": "https://example.com/article"}

The server fetches the page from inside the data centre. Nothing stops the user from sending an address that only the server can reach:

{"url": "http://localhost:8080/admin"}
{"url": "http://10.0.3.17:9200/_cat/indices"}
{"url": "http://169.254.169.254/latest/meta-data/"}

The first two hit internal services that trust anything on the private network. The third is the one attackers want most.

Why cloud metadata is the prize

Cloud providers run a metadata service at a link-local address, 169.254.169.254, reachable from every virtual machine. It tells the machine about itself, and on many setups it also hands out temporary credentials for the machine's role. If your app server can read that address and an attacker can steer its requests, the attacker can ask for those credentials and then call your cloud account's API with whatever permissions the role has.

That is how a single preview feature becomes access to storage buckets, queues or databases.

Filters that do not work

Teams often try to block "bad" addresses by looking at the string the user sent. These checks are easy to get around:

  • Blocking localhost and 127.0.0.1: the same address can be written as 127.1, 2130706433, 0x7f000001 or [::1].
  • Blocking by hostname: an attacker registers a domain whose DNS record points at 169.254.169.254.
  • Checking DNS once, then fetching: the name resolves to a public address during the check and to an internal one a second later (DNS rebinding).
  • Validating only the first URL: the server follows a redirect from an innocent public page to an internal address.

The lesson: decide on the resolved IP address of the actual connection, not on the text of the URL.

Defences that hold up

1. Allow-list where you can. If the feature only needs to talk to a few partners, accept only those hosts. This is the strongest control and the simplest one.

2. Resolve, check, then connect to the address you checked. When you must accept arbitrary URLs:

  • allow only http and https;
  • resolve the host yourself and reject private, loopback, link-local and reserved ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, 169.254.0.0/16, ::1, fc00::/7, fe80::/10);
  • connect to that exact IP, so a second DNS answer cannot change the destination;
  • turn off automatic redirects, or run the same check on every hop.

3. Harden the metadata service. On AWS, require IMDSv2, which needs a session token obtained with a PUT request and a custom header. Most SSRF bugs can only send simple GET requests, so they cannot get the token. Set the hop limit to 1 so containers on the host cannot reach it either. Other clouds have similar settings; turn them on.

4. Isolate the fetcher. Run URL-fetching code in a separate service or network segment with no route to internal systems and no cloud role. If it is tricked, there is nothing behind it.

5. Give the role less to lose. The credentials on an app server should allow what that server needs and nothing more. SSRF plus an admin role is a full compromise; SSRF plus a narrow role is a contained bug.

6. Do not return the raw response. Send back only what the feature needs, such as a title and an image. "Blind" SSRF is still dangerous, but echoing internal responses makes exploitation trivial.

Testing your own app

Look for any parameter that holds a URL, a hostname or a file path: url, uri, callback, webhook, feed, image, redirect. Point it at a server you control first to confirm the request comes from your backend, then try internal addresses and redirects in a test environment. Write down what you find and fix the pattern, not just the one endpoint.

SSRF is rarely a bug in one line. It is a missing boundary between "things users can name" and "things only the server should reach". Draw that boundary in the network and in the code, and the URL field goes back to being harmless.

Keep reading