SSRF

SSRF, Explained: When Your Server Becomes the Attacker's Proxy

Server-side request forgery turns a trusted server into a request-making proxy for the attacker, reaching internal services and cloud credentials the internet should never touch. Here is how it works and how to lock it down.

SelfSec Team3 min read
Part 1 of 4from theSSRFseries
http://10.0.0.5:8080/admin
On this page

Server-side request forgery (SSRF) happens when an application can be tricked into making HTTP — or other-protocol — requests to a destination the attacker chooses. The request leaves from the server, so it carries the server's network position and trust. Anything the application can reach from inside the perimeter, the attacker can now reach too: internal-only admin panels, databases, and the cloud instance-metadata service that hands out credentials.

That last point is why SSRF earned its own slot in the OWASP Top 10. In cloud environments it is one of the most reliable paths from a single vulnerable parameter to full account compromise. This post walks through how it happens, what an attacker does with it, and how to stop it.

How it happens

The root cause is simple: the application takes a user-supplied URL and fetches it without checking where it points. Think of a feature that generates link previews, imports an image from a URL, or proxies a webhook:

@app.route("/fetch")
def fetch():
    url = request.args.get("url")
    resp = requests.get(url)
    return resp.content

The developer pictured url being https://example.com/logo.png. But nothing forces that. The value is fully attacker-controlled, and the server will dutifully request whatever it is given — including addresses that only exist inside the network.

A concrete attack

On a cloud host, the attacker does not aim at the public internet. They aim inward, at the instance-metadata endpoint that every major cloud exposes on a link-local address:

GET /fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/ HTTP/1.1
Host: app.example

The server connects to 169.254.169.254 from inside the VPC, the metadata service answers because the request came from the instance itself, and the response — temporary IAM access keys — gets handed straight back to the attacker. From there they can act as the application's cloud role.

The same trick reaches internal services. Pointing url at http://127.0.0.1:6379 or http://10.0.0.5:8080/admin lets the attacker probe ports and talk to back-ends that were never meant to face the internet. Naive filters that just block the string 127.0.0.1 are easy to slip past:

GET /fetch?url=http://2130706433/        # decimal form of 127.0.0.1
GET /fetch?url=http://0x7f000001/        # hex form
GET /fetch?url=http://0177.0.0.1/        # octal form
GET /fetch?url=http://[::1]/             # IPv6 loopback
GET /fetch?url=http://[email protected]/   # userinfo trick

The attacker never gained access to the internal network. They borrowed the server's access — every request is made with the server's identity, from inside the firewall.

What an attacker gains

Once the server can be pointed anywhere, the blast radius is large:

  • Cloud credential theft — reading IAM tokens from 169.254.169.254 and pivoting to the whole cloud account.
  • Internal service access — reaching admin panels, dashboards and databases bound to localhost or private ranges.
  • Port scanning and enumeration — mapping the internal network by watching which addresses respond.
  • Protocol smuggling — using gopher://, dict:// or file:// to talk to Redis, hit other TCP services, or read local files like /etc/passwd.

The fix: allowlist destinations, block the internal world

Blocklists lose against the long tail of IP encodings and URL tricks. The reliable approach is to decide up front exactly where the server is allowed to go, and refuse everything else. Resolve the hostname, check the resolved IP against an allowlist (and against blocked link-local and private ranges), and only then make the request:

import ipaddress, socket
from urllib.parse import urlparse

ALLOWED_HOSTS = {"images.example.com", "cdn.example.com"}

def safe_fetch(url):
    parsed = urlparse(url)
    if parsed.scheme not in ("https",):
        raise ValueError("scheme not allowed")
    if parsed.hostname not in ALLOWED_HOSTS:
        raise ValueError("host not allowed")

    resolved = ipaddress.ip_address(socket.gethostbyname(parsed.hostname))
    if resolved.is_private or resolved.is_loopback or resolved.is_link_local:
        raise ValueError("internal address blocked")

    return requests.get(url, allow_redirects=False, timeout=5).content

Reinforce it with a few more controls: disable URL schemes you do not need (only https survives above, killing file, gopher and dict), do not blindly follow redirects (a 302 to 169.254.169.254 defeats a host check made before the redirect), and require the IMDSv2 token flow so the metadata service is not readable by a plain GET. Where possible, route outbound fetches through an egress proxy that enforces the allowlist for you.

How SelfSec proves this one

Scanner behaviour
Module
SSRF
How it probes
Rewrites URL-bearing parameters toward internal ranges, cloud metadata endpoints and attacker-controlled hosts.
How it confirms
A DNS or HTTP callback originating from the target's own infrastructure confirms the blind cases no response body reveals.

Reported as CWE-918 · ATT&CK T1190 · SARIF 2.1.0

A reflected response alone is not enough to promote a SSRF finding — the classical engine has to confirm the behaviour before it reaches the report. See the detection engine

SelfSec is intended strictly for authorized security testing of systems you own or are explicitly permitted to assess.

Do both things about SSRF

SelfSec covers this class from both sides — the scanner confirms it in your own app, the firewall blocks it in front of your origin while the fix ships.

The SSRF series

  1. 01OverviewSSRF, Explained: When Your Server Becomes the Attacker's ProxyReading
  2. 02BlindBlind and Out-of-Band SSRF: Detecting the Request You Can't See5 min
  3. 03Cloud MetadataCloud-Metadata SSRF: From One URL to Full Account Takeover5 min
  4. 04Protocol SmugglingProtocol Smuggling in SSRF: gopher, dict and file Schemes5 min

Related reading