In August 2025, Microsoft patched CVE-2025–53767 in Azure OpenAI. The vulnerability scored a perfect 10.0 on CVSS. No authentication required. An attacker could craft a request that pointed the Azure OpenAI service at the Instance Metadata Service endpoint, extract managed identity tokens and use those tokens to move laterally through the Azure tenant. The root cause was a URL parameter the service would fetch without validating where the resulting HTTP request actually went.

That is SSRF: Server-Side Request Forgery. It is not a new class of vulnerability. It earned its spot as API10 in the OWASP API Security Top 10 2023. Yet according to SonicWall's 2025 Cyber Threat Report, SSRF attacks increased 452% between 2023 and 2024. That number does not mean attackers invented a new technique. It means the attack surface grew while defenses stayed where they were.

This is a guide for backend engineers who write code that fetches URLs. Not for pentesters. Not for security researchers hunting bug bounties. For the engineers who build webhook receivers, document importers and URL preview endpoints: the code paths where SSRF lives before it becomes someone else's incident report.

What SSRF Actually Looks Like

The pattern is simple. Your application receives a URL as input and makes an HTTP request to that URL on behalf of the user:

// Vulnerable: no validation on where this request goes
$url = $request->input('url');
$response = Http::get($url);
return $response->body();

The user provides https://my-document.s3.amazonaws.com/file.pdf. Your server fetches it. That is the intended behavior.

Then the user provides http://169.254.169.254/latest/meta-data/iam/security-credentials/my-role.

Your server fetches that too. The response is a JSON blob containing AccessKeyId, SecretAccessKey and Token. These are credentials with whatever IAM permissions the instance role carries. If that role can list S3 buckets, read from Secrets Manager or invoke Lambda, the attacker now has those capabilities.

This endpoint (169.254.169.254) is the AWS Instance Metadata Service (IMDS). Azure uses the same IP. GCP uses metadata.google.internal. Every cloud instance has one. It is accessible only from within the instance, and on most older configurations, a single unauthenticated GET request is all it takes to retrieve credentials.

Beyond the metadata service, SSRF reaches anything your application server can talk to over the network. Internal Redis with no password. Elasticsearch with open HTTP APIs. Admin panels bound to localhost. Internal microservices that trust all inbound VPC traffic. Kubernetes API servers running on cluster-internal IPs. Internal Prometheus exporters. The attack surface is not a known URL. It is everything your server can reach.

The Attack Surface That Keeps Expanding

The 452% increase in SSRF attacks from 2023 to 2024 has a structural explanation. Several things happened in parallel.

Cloud-native deployments became the default. More applications run on EC2 instances, Azure VMs or GCP compute with instance metadata available at the link-local address. More applications have attached IAM roles with real permissions attached to them.

Microservices proliferated. Each internal service boundary is a new internal HTTP API. Those APIs often trust inbound traffic from within the network without requiring authentication, because the assumption is "we're behind a firewall." SSRF gives an attacker a foothold inside that boundary without ever touching the firewall.

AI-powered services added URL-fetching paths at scale. Services that accept documents, images or external data from user-supplied URLs create new SSRF surfaces. CVE-2025–53767 was not in an obscure side project. It was in Azure OpenAI, one of the largest production AI deployments in the world. The URL-fetching logic existed to serve legitimate features. The validation did not exist to constrain where those fetches could go.

AI-assisted exploitation tooling also lowered the skill floor. Attackers enumerate SSRF targets systematically using tools that probe hundreds of internal endpoints. What once required a skilled operator now runs mostly automated.

The Fixes That Do Not Hold

Most SSRF "fixes" address the obvious cases and leave the interesting cases open. Here is what developers reach for, and where each approach fails.

IP blocklists

The first instinct is to block known internal addresses by string comparison:

// Looks reasonable. Is not sufficient.
$host = parse_url($url, PHP_URL_HOST);
$ip   = gethostbyname($host);

$blocked = ['127.0.0.1', 'localhost', '169.254.169.254', '10.0.0.1'];
if (in_array($ip, $blocked)) {
    abort(400, 'Blocked');
}

An attacker supplies http://2852039166/latest/meta-data/. The integer 2852039166 is the decimal representation of 169.254.169.254. PHP resolves it to the same address, and it is not in your blocklist. The hex equivalent (0xa9fea9fe) works the same way. Octal too. You cannot maintain a blocklist that covers every alternative representation of every address you want to block.

Hostname-only validation

Some teams blocklist hostnames rather than IPs: reject 169.254.169.254, metadata.google.internal and known internal DNS names. Attackers use DNS rebinding to bypass this. The attacker registers safe.attacker.com with a very short TTL. During your DNS resolution check, it resolves to a safe IP that passes validation. The actual HTTP request fires after that TTL expires. The DNS response now returns 169.254.169.254. The request goes to the metadata service.

Pre-request DNS validation

Resolving the hostname and checking the resulting IP before making the request is closer to correct. But there is a time-of-check-time-of-use window between the DNS check and the actual HTTP request. If an attacker controls the DNS TTL for their domain and your resolver does not cache aggressively, they can thread DNS rebinding through that window.

Redirect following

Many HTTP client libraries follow redirects by default. You validate the initial URL, it passes. The target server responds with 302 Location: http://169.254.169.254/. Your HTTP client follows the redirect to the metadata service. The validation never saw the final destination.

Defense attempt What it blocks What bypasses it IP string blocklist Obvious IPs typed as dotted decimal Decimal, hex, octal representations Hostname blocklist Known internal hostnames DNS rebinding, direct IP literals Pre-request DNS check Basic hostname-to-IP SSRF DNS rebinding TOCTOU window URL validation only Straightforward SSRF URLs 302 redirects to internal IPs

Real Vulnerabilities That Made It to Production

CVE-2025–53767 and CVE-2026–30832 both follow the same pattern: URL-fetching code inside a feature that had nothing to do with security. Azure OpenAI fetched URLs to process content. Soft Serve, Charmbracelet's self-hosted Git server, fetched LFS endpoints as part of repository import.

CVE Affected product CVSS Attack vector Potential impact CVE-2025–53767 Azure OpenAI 10.0 (Critical) Unauthenticated, network Azure managed identity token exfiltration CVE-2026–30832 Soft Serve 0.6.0 to 0.11.3 9.1 (Critical) Authenticated SSH user via --lfs-endpoint Internal port scan, cloud credential theft via IMDS

CVE-2026–30832 is worth examining closely because the exploitation path is blind. An authenticated SSH user supplies a crafted --lfs-endpoint URL when importing a repository. The server makes an HTTP request to that endpoint. Even if the response is not valid LFS JSON, the request was still made. The attacker does not need a response body. They need the request to fire at an internal target, which is enough for port enumeration and for retrieving credentials from the metadata endpoint. Charmbracelet patched this in v0.11.4 within days of public disclosure.

Defense That Actually Holds

Three layers. All three are required. Any single layer is insufficient.

Layer 1: Allowlists at the application layer

An allowlist specifies exactly which hosts your application should ever fetch. Everything else is rejected at the application level before any network request is made.

// Allowlist + post-resolution IP validation
function isSafeUrl(string $url): bool
{
    $parsed = parse_url($url);
    if (!isset($parsed['host'])) return false;
    if (($parsed['scheme'] ?? '') !== 'https') return false;

    // Explicitly define what hosts this feature is allowed to talk to

    $allowedHosts = ['api.stripe.com', 'hooks.slack.com'];
    if (!in_array($parsed['host'], $allowedHosts)) return false;
    // Resolve DNS and re-validate the IP. Best-effort protection against rebinding.

    $ip = gethostbyname($parsed['host']);
    if ($ip === $parsed['host']) return false; // DNS resolution failed
    return filter_var(
        $ip,
        FILTER_VALIDATE_IP,
        FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
    ) !== false;
}

FILTER_FLAG_NO_PRIV_RANGE rejects RFC1918 ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16). FILTER_FLAG_NO_RES_RANGE rejects reserved ranges including 0.0.0.0/8, 169.254.0.0/16 and 127.0.0.0/8. Consult the current PHP documentation for the full list of covered ranges before deploying.

For Python, the same logic looks like this:

import ipaddress
import socket
from urllib.parse import urlparse

ALLOWED_HOSTS = {'api.stripe.com', 'hooks.slack.com'}
def is_safe_url(url: str) -> bool:
    parsed = urlparse(url)
    if parsed.scheme != 'https':
        return False
    host = parsed.hostname
    if host not in ALLOWED_HOSTS:
        return False
    try:
        ip = socket.gethostbyname(host)
        addr = ipaddress.ip_address(ip)
        # is_private covers RFC1918; is_link_local covers 169.254.0.0/16
        return not (
            addr.is_private
            or addr.is_loopback
            or addr.is_link_local
            or addr.is_reserved
        )
    except (socket.gaierror, ValueError):
        return False

Disable redirect following in your HTTP client. In curl, set CURLOPT_FOLLOWLOCATION to false. In Python's requests library, pass allow_redirects=False. An SSRF exploit that requires redirect following to reach the metadata service is an SSRF exploit your HTTP client should refuse to complete.

The post-resolution IP check is best-effort, not a guarantee. DNS rebinding can still win through the TOCTOU window. That is what Layer 2 is for.

Layer 2: Network egress controls

This is the layer that survives when your application code is wrong. Your application servers should not be able to reach private IP ranges or the metadata endpoint at the network layer, regardless of what your application code does or does not validate.

Block outbound traffic from application instances to:

  • 127.0.0.0/8 (loopback)
  • 10.0.0.0/8 (RFC1918)
  • 172.16.0.0/12 (RFC1918)
  • 192.168.0.0/16 (RFC1918)
  • 169.254.0.0/16 (link-local, covers cloud metadata endpoints)
  • ::1/128 (IPv6 loopback)
  • fc00::/7 (IPv6 unique local addresses)

On AWS, this is a security group egress rule. On GCP, a VPC firewall egress rule. On a bare VPS, an iptables OUTPUT rule. The specific implementation differs. The outcome is the same: even if your application code makes a request to an internal IP, the network drops it.

This is not a belt-and-suspenders redundancy. It is the safety net that catches DNS rebinding attacks after they slip through your application-layer TOCTOU window.

Layer 3: IMDSv2 on AWS (and equivalent controls on other clouds)

If you run on AWS, enforce IMDSv2 on all instances. IMDSv2 requires a session token obtained through a PUT request with a custom header (X-aws-ec2-metadata-token-ttl-seconds) before any metadata read will succeed. Most SSRF-based metadata retrieval uses GET requests and cannot set custom headers. IMDSv2 breaks this attack path at the cloud infrastructure layer, independently of your application code and independently of your network rules.

AWS made IMDSv2 the default for instances launched with Amazon Linux 2023. For existing instances, enforce it at the instance level via aws ec2 modify-instance-metadata-options, or block IMDSv1 access through an SCP at the organization level. Azure has equivalent metadata service access controls. GCP requires a Metadata-Flavor: Google header on all metadata requests by default.

Enforcing IMDSv2 means that even a fully successful SSRF attack against the metadata endpoint returns an error rather than credentials. This does not eliminate SSRF as a vulnerability. It eliminates the highest-value target SSRF has been used to reach.

What This Means for Your Codebase

Every feature that fetches a URL the application doesn't control is a potential SSRF surface. Webhook registration. Document importers. Avatar URL fields. Link preview generators. RSS feed readers. Any endpoint that calls out to the network based on user input.

The question is not whether your URL validation is strict enough. The question is whether your network can survive your URL validation being wrong.

Application-layer checks are your first line. Network egress controls are your last line. The distance between a SSRF finding on a security scanner and a production incident is usually the absence of the second line.

CVE-2025–53767 scored 10.0 not because the code was carelessly written. It scored 10.0 because the metadata endpoint was reachable from the application server, the credentials behind it were valuable and there was no network-layer backstop in place to block the request regardless of what the application decided.

Put the network backstop in place before you need it.