Server-side request forgery has been a known vulnerability class for a long time, but it went from a moderate concern to a top-tier one once applications moved to the cloud, because the blast radius changed. A decade ago, tricking a server into making an unintended outbound request might expose an internal admin panel. Today, that same trick can reach a cloud provider's instance metadata service and walk out with credentials that grant far broader access than the original application ever had.

The core mechanism

SSRF happens whenever an application takes a URL, hostname, or IP address from user input and uses it to make a server-side request — fetching a webhook callback, rendering a preview of a linked page, proxying an image, importing data from a remote file. If the application doesn't restrict which destinations that request can reach, an attacker can point it somewhere it was never meant to go: an internal service with no authentication because it assumed only trusted internal callers could reach it, a database admin interface bound to localhost, or a cloud metadata endpoint that hands out credentials to anything running on that instance.

The reason this class of bug is so dangerous specifically in cloud environments is that instance metadata services traditionally trusted any request originating from the instance itself, with no additional authentication required. A server tricked into requesting the metadata endpoint on the application's behalf could retrieve the temporary IAM credentials attached to that instance — credentials the application code never directly handles and the attacker could never have obtained any other way.

Defenses that actually work

Allowlisting destinations is the strongest defense when the application's legitimate use case is narrow — if a webhook feature only ever needs to reach a known set of partner domains, enforce that allowlist server-side rather than trying to blocklist the internal ranges you want to exclude. Blocklisting is inherently weaker because it requires enumerating every way an attacker could reach an internal address, including redirects, DNS rebinding, alternate IP representations, and IPv6 equivalents of blocked IPv4 ranges — it's easy to miss one.

python
import ipaddress
import socket
from urllib.parse import urlparse

BLOCKED_NETWORKS = [
    ipaddress.ip_network("169.254.0.0/16"),  # link-local, incl. cloud metadata
    ipaddress.ip_network("10.0.0.0/8"),
    ipaddress.ip_network("172.16.0.0/12"),
    ipaddress.ip_network("192.168.0.0/16"),
    ipaddress.ip_network("127.0.0.0/8"),
]

def is_safe_destination(url):
    host = urlparse(url).hostname
    resolved_ip = ipaddress.ip_address(socket.gethostbyname(host))
    return not any(resolved_ip in net for net in BLOCKED_NETWORKS)

Resolve the hostname and validate the resulting IP immediately before making the request, not just at input validation time — this closes the DNS rebinding gap where a domain resolves to a safe IP during validation and a blocked one at request time. Disable HTTP redirect following, or re-validate the destination after every redirect hop, since an allowlisted domain can still redirect to an internal address if redirects aren't checked at each step.

Defense in depth beyond input validation

Migrate to Instance Metadata Service v2 (IMDSv2) or your cloud provider's equivalent, which requires a session token obtained via a PUT request — a step that basic SSRF via a simple GET-based request forwarding can't complete, meaningfully raising the bar even if application-level validation has a gap. Run the component that makes outbound requests with the least privilege it needs, so a successful SSRF that does reach an internal service is limited in what it can do with any credentials it retrieves. Network-level segmentation — placing services that fetch external URLs in a network segment without a direct path to sensitive internal systems — contains the damage even when the application-layer defense is imperfect, and it's worth treating as a required layer rather than a backup for input validation that should have caught everything itself.