SSRF (CWE-918, OWASP A10:2021) forces a server to request internal resources — cloud metadata credentials, IMDSv1 tokens, and internal services exposed to full takeover.
TL;DR
169.254.169.254 yields IAM credentials in one HTTP request (IMDSv1)2130706433, octal 0177.0.0.1, IPv6 ::ffff:127.0.0.1 all bypass string-match filtersServer-Side Request Forgery (CWE-918) occurs when an attacker causes a server to make HTTP requests to a destination of the attacker's choosing. The server becomes an involuntary proxy: its outbound request originates from a trusted internal IP, bypassing firewalls, VPC security groups, and network-layer access controls that would block the same request from an external client.
SSRF is categorically different from CSRF and Open Redirect. CSRF tricks a user's browser into sending a forged request but cannot read the response. Open Redirect causes a user's browser to follow an attacker-controlled URL, causing a client-side navigation. SSRF bypasses all of these — the vulnerable server itself fetches the resource, granting access to internal endpoints, metadata services, and localhost that are invisible to the public internet. Unlike SQL injection or code injection, SSRF requires no special server-side execution capability — any HTTP client library becomes the attack vector.
SSRF earned its own OWASP Top 10 entry (A10:2021) for the first time in 2021, reflecting a structural shift: cloud deployments created a universally exploitable attack surface. Every EC2, ECS, Lambda, GCE instance, and Azure VM exposes a link-local metadata service reachable only from within the instance. SSRF is the only external path to that endpoint. SonicWall's 2025 Cyber Threat Report documents a 452% increase in SSRF attacks from 2023 to 2024, correlated with AI-assisted exploitation tooling. As of late 2024, only 32% of EC2 instances enforce IMDSv2 — meaning roughly two-thirds of AWS workloads remain vulnerable to single-request credential theft.
An application accepts a URL parameter and passes it directly to a server-side HTTP client. The server's internal network position grants it access to services the attacker cannot reach directly.
The attack proceeds in four steps:
url, callback, webhook, image_url, redirect, feed, endpoint).requests.get(), fetch(), HttpURLConnection, net/http.Get()) without validation.A minimal in-band SSRF against the AWS metadata service:
POST /api/fetch-preview HTTP/1.1
Host: app.example.com
Content-Type: application/json
{"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/MyApp-EC2Role"}HTTP/1.1 200 OK
Content-Type: application/json
{
"AccessKeyId": "ASIAIOSFODNN7EXAMPLE",
"SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"Token": "EXAMPLE_SESSION_TOKEN_TRUNCATED...",
"Expiration": "2026-05-09T18:00:00Z"
}These credentials are valid IAM credentials — usable with aws s3 ls, aws iam list-roles, aws ec2 describe-instances, and any other AWS API call within the role's permission boundary.
| Variant | Technique | Impact | Dedicated Page |
|---|---|---|---|
| Basic In-Band | URL parameter → direct response return | Cloud credentials, internal service read | /learn/ssrf-basic |
| Blind / OOB | URL parameter → OOB DNS/HTTP callback | Internal topology, blind data exfil | /learn/ssrf-blind |
| Cloud Metadata | IMDS endpoints (AWS/GCP/Azure) | IAM credential theft → full cloud takeover | /learn/ssrf-cloud-metadata |
| Protocol Smuggling | gopher://, file://, dict:// | Redis RCE, local file read | /learn/ssrf-protocol-smuggling |
| Header-Based | X-Forwarded-Host, Forwarded injection | Internal routing bypass, SSRF via CRLF | /learn/ssrf-header-based |
Basic in-band SSRF is the most directly exploitable form. The server returns the fetched response verbatim. A URL parameter named url, src, image, remote, feed, or webhook submitting http://169.254.169.254/latest/meta-data/ is the canonical starting point. The 2019 Capital One breach followed exactly this pattern.
Blind SSRF yields no response to the attacker. Detection requires an OOB listener (Burp Collaborator, Interactsh). A DNS-only callback confirms the server resolves the hostname but HTTP is blocked — POTENTIAL, not CONFIRMED. An HTTP callback confirms a live outbound request — CONFIRMED HIGH. Data exfiltrated in the OOB body (AWS credentials in the URL path) — CONFIRMED CRITICAL.
Protocol smuggling elevates SSRF to RCE when the server uses a multi-protocol HTTP client. Python urllib, Java java.net.URL, and curl all support gopher://, file://, and dict://. Submitting gopher://127.0.0.1:6379/_<redis-commands> sends raw TCP bytes directly to an internal Redis instance, enabling cron-based code execution.
Header-based SSRF occurs when HTTP headers containing hostname values are consumed server-side. CVE-2026-27739 (Angular SSR, CVSS 9.2): injecting X-Forwarded-Host: attacker.com causes the SSR layer to make an outbound request to http://attacker.com/api/config. GitLab CVE-2025-6454 (CVSS 8.5): CRLF injection in webhook custom headers allows injecting Metadata: true as a separate header, bypassing Azure IMDS protection.
Cloud metadata services are link-local HTTP APIs, reachable only from within the instance. SSRF is the sole external attack path.
| Cloud | Primary Endpoint | Required Header | Credential Path |
|---|---|---|---|
| AWS IMDSv1 | http://169.254.169.254/latest/meta-data/ | None | iam/security-credentials/{ROLE} |
| AWS IMDSv2 | Same IP, PUT token flow | X-aws-ec2-metadata-token-ttl-seconds: 21600 | Same after token |
| AWS ECS/Fargate | http://169.254.170.2/v2/credentials/{GUID} | None | GUID from AWS_CONTAINER_CREDENTIALS_RELATIVE_URI env var |
| GCP | http://metadata.google.internal/computeMetadata/v1/ | Metadata-Flavor: Google | instance/service-accounts/default/token |
| Azure IMDS | http://169.254.169.254/metadata/instance?api-version=2021-02-01 | Metadata: true | identity/oauth2/token?resource=... |
| Azure WireServer | http://168.63.129.16/?comp=versions | None (no auth) | 168.63.129.16:32526/vmSettings → encrypted secrets |
| DigitalOcean | http://169.254.169.254/metadata/v1.json | None | Full JSON including root SSH key |
| Kubernetes etcd | http://127.0.0.1:2379/v3/kv/range (POST) | None | Full cluster secrets dump |
IMDSv2 bypass conditions: IMDSv2 blocks naive single-GET SSRF by requiring a PUT with X-aws-ec2-metadata-token-ttl-seconds to obtain a session token. Four bypass conditions exist: (1) header-injection SSRF (CVE-2019-8451 Atlassian Confluence pattern) allows injecting the PUT header; (2) multi-request SSRF chains PUT then GET; (3) headless Chrome PDF generators execute the full IMDSv2 flow via JavaScript fetch() — PUT token, then GET credentials; (4) redirect chains issue the PUT at the redirect server.
AWS IMDSv2 is necessary but not sufficient. Enforcing HttpTokens: required per-instance blocks GET-only SSRF. The reliable control is an AWS Service Control Policy (SCP) denying ec2:RunInstances when ec2:MetadataHttpTokens != required — applied organization-wide. Additionally, --http-put-response-hop-limit 1 blocks metadata access from containerized workloads unless the hop limit is explicitly raised.
Blocklist-based SSRF filters that check for 127.0.0.1, localhost, or 169.254 are bypassed by encoding, parsing differentials, and redirect chains.
All of the following resolve to 127.0.0.1 (loopback) but bypass string-match filters:
http://2130706433/ # decimal DWORD
http://0177.0.0.1/ # octal per-octet
http://0x7f000001/ # hex single value
http://127.1/ # 3-octet shorthand
http://[::1]/ # IPv6 loopback
http://[::ffff:127.0.0.1]/ # IPv6-mapped IPv4
http://[::ffff:7f00:1]/ # CVE-2025-9960: IPv6 hextet form — bypasses IPv4-only blocklists
http://localhost./ # trailing FQDN dot — bypasses NO_PROXY literal match (CVE-2025-62718)
http://localtest.me/ # public DNS service resolving to 127.0.0.1For 169.254.169.254 (AWS IMDS), equivalent encodings:
http://2852039166/ # decimal DWORD
http://0xA9FEA9FE/ # hex
http://0251.0376.0251.0376/ # octal
http://[::ffff:169.254.169.254]/ # IPv6-mapped
http://[::ffff:a9fe:a9fe]/ # IPv6 hexValidation layer and HTTP library may parse the same string differently:
http://trusted.com@169.254.169.254/ # validator sees trusted.com as host; requester uses IP
http://169.254.169.254#@trusted.com/ # fragment confusion — validator fails on # host
http://169.254.169.254\trusted.com/ # WHATWG treats \ as / — bypasses suffix checks
http://trusted.com.evil.com/ # satisfies endsWith('trusted.com') checkCVE-2024-29415 (Node.js ip package, unmaintained): ip.isPublic() classifies 127.1, ::fFFf:127.0.0.1, and 01200034567 as globally routable, providing zero SSRF protection for any application relying on it.
CVE-2025-62718 (Axios < 1.15.0, CVSS 9.3): NO_PROXY rules use literal string comparison. http://localhost.:8080/ (trailing dot) and http://[::1]:8080/ (IPv6 loopback) bypass all NO_PROXY rules, routing requests through an attacker-controlled proxy.
Validators that check only the initial URL and follow redirects without re-validation:
https://302.r3dir.me/?r=http://169.254.169.254/latest/meta-data/
https://307.r3dir.me/?r=http://169.254.169.254/latest/meta-data/
# 307 preserves HTTP method — critical for POST-based SSRFDNS rebinding defeats allowlist + DNS resolution validation when validation and request happen at different times. The validator resolves attacker.com → 1.2.3.4 (public IP, passes allowlist). The attacker immediately repoints DNS with TTL=0 to 169.254.169.254. The server makes the actual request, re-resolves, and hits the metadata service. CVE-2026-27127 (Craft CMS) demonstrates this bypass against a production-deployed SSRF patch. The defense: resolve DNS once, then pin the resolved IP and force the HTTP connection to that IP — never re-resolve at request time.
| CVE / Incident | Product | CVSS | Vector | Year |
|---|---|---|---|---|
| Capital One breach | AWS EC2 WAF | — | IMDSv1 → IAM creds → S3 exfil | 2019 |
| CVE-2025-62718 | Axios < 1.15.0 | 9.3 Critical | NO_PROXY bypass → proxy SSRF | 2025 |
| CVE-2025-6454 | GitLab CE/EE | 8.5 High | Webhook CRLF injection → blind SSRF | 2025 |
| CVE-2024-8977 | GitLab EE | 8.2 High | Analytics Dashboard Cube API → internal SSRF | 2024 |
| CVE-2025-4123 | Grafana Image Renderer | 7.6 High | Open redirect → Image Renderer full-read SSRF | 2025 |
| CVE-2025-68437 | Craft CMS | Medium | GraphQL saveAssets._file.url → cloud metadata | 2025 |
| CVE-2026-27127 | Craft CMS | High | DNS rebinding TOCTOU → bypasses SSRF patch | 2026 |
| CVE-2024-40898 | Apache HTTP Server (Windows) | High | mod_rewrite → SMB NTLM hash leak | 2024 |
| HackerOne #2301565 | (undisclosed) | — | Webhook SSRF → $2,500 bounty | 2024 |
| HackerOne #3176157 | PortSwigger | — | DNS rebinding → internal network access → $2,000 | 2024 |
Capital One 2019 remains the canonical SSRF case study. A WAF instance running on EC2 had an SSRF vulnerability. The attacker queried http://169.254.169.254/latest/meta-data/iam/security-credentials/ISRM-WAF-Role via a single unauthenticated HTTP request. The returned temporary IAM credentials had excessive S3 permissions — the attacker enumerated 700+ buckets and exfiltrated 30 GB of data covering 106 million customer records. Regulatory outcome: $80M OCC fine, $190M class action settlement. Root cause: IMDSv1 (no authentication) + overprivileged IAM role + no network egress controls.
CVE-2025-62718 — Axios NO_PROXY bypass (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H = 9.3 Critical): Axios performs literal string comparison when evaluating NO_PROXY rules per RFC 1034 §3.1. A trailing dot on FQDNs (localhost. vs localhost) is valid per RFC 3986 §3.2.2 but bypasses Axios NO_PROXY checks in all versions before 1.15.0. Any Node.js backend using Axios with proxy configuration — a common enterprise pattern for outbound filtering — was fully exposed to SSRF via http://localhost.:PORT/ or http://[::1]:PORT/ requests.
CVE-2025-6454 — GitLab CRLF injection → SSRF (CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H = 8.5): GitLab 16.11 introduced custom webhook headers without CRLF sanitization. An authenticated Developer could inject \r\n sequences into header names, inserting arbitrary headers into the serialized outbound webhook payload. In proxy-based enterprise deployments, injecting \r\nMetadata: true\r\n as a second header bypassed Azure IMDS protection and reached the managed identity credential endpoint.
CVE-2025-4123 — Grafana open redirect → SSRF: Grafana v10.4 through 12.0 contained a client-side path traversal enabling open redirect. When the Grafana Image Renderer plugin is installed, the renderer fetches the redirected URL server-side — converting a client-side open redirect into a full-read SSRF. The renderer accessed 169.254.169.254 and returned AWS IAM credentials as part of the rendered image. One-third of all Grafana instances were estimated vulnerable at disclosure.
url, uri, redirect, callback, webhook, image_url, avatar, feed, endpoint, src, target, dest, remote. Also test JSON body fields: webhook, callback_url, endpoint, image, redirect.http://169.254.169.254/latest/meta-data/
http://localhost/
http://[::1]/
http://192.168.0.1/
file:///etc/passwdAccessKeyId, SecretAccessKey), internal service banners (Grafana, elasticsearch, "cluster_name"), or RFC-1918 address content.http://169.254.169.254/ (fast TCP RST or SYN-ACK) and http://192.0.2.1/ (RFC 5737 documentation IP, guaranteed unreachable, 30s SYN-timeout). A statistically significant differential (Welch t-test p < 0.05) confirms an outbound connection attempt.Host, X-Forwarded-Host, X-Forwarded-For, Referer, and X-Original-URL.Burp Suite Pro: the active scanner automatically injects Burp Collaborator URLs into all parameters and headers, flags DNS and HTTP callbacks as SSRF findings. Burp Collaborator distinguishes DNS-only (potential) from HTTP (confirmed).
Interactsh (ProjectDiscovery, open-source): self-hostable OOB infrastructure for pipeline automation. Generates unique per-parameter tokens — a DNS resolution without HTTP remains POTENTIAL; an HTTP GET/POST is CONFIRMED.
Semgrep (semgrep --config p/python): flags requests.get($URL) with user-controlled $URL, unvalidated urllib.request.urlopen(), and httpx.get() taint paths. CodeQL SSRF queries trace full data-flow from HTTP input to HTTP client sink across function boundaries.
BreachVex detects SSRF via per-parameter out-of-band callbacks (one unique token per URL parameter, capped at 10), response differential analysis (body-length ratio plus a timing threshold), cloud metadata content matching, and a graduated proof model: a DNS-only callback is potential, an HTTP callback is confirmed high-severity, and out-of-band data exfiltration is confirmed critical.
An allowlist is the only reliable SSRF defense. Blocklists have too many encoding, alias, and DNS rebinding bypasses to be effective. The allowlist must be validated after DNS resolution — not just on the hostname string.
import ipaddress
import socket
import urllib.parse
import requests
ALLOWED_HOSTS = frozenset({"api.stripe.com", "hooks.slack.com", "cdn.example.com"})
class SSRFError(ValueError):
pass
def validate_and_fetch(url: str) -> str:
parsed = urllib.parse.urlparse(url)
# 1. Scheme allowlist — blocks gopher://, file://, dict://, ftp://
if parsed.scheme not in ("http", "https"):
raise SSRFError(f"Disallowed scheme: {parsed.scheme!r}")
host = (parsed.hostname or "").rstrip(".") # strip trailing FQDN dot (CVE-2025-62718 class)
if not host:
raise SSRFError("Missing hostname")
# 2. Exact hostname allowlist (not contains / startsWith)
if host not in ALLOWED_HOSTS:
raise SSRFError(f"Host not in allowlist: {host!r}")
# 3. DNS resolution — validate every returned IP address
try:
results = socket.getaddrinfo(host, None, proto=socket.IPPROTO_TCP)
except socket.gaierror as e:
raise SSRFError(f"DNS resolution failed: {e}")
for (_, _, _, _, sockaddr) in results:
ip = ipaddress.ip_address(sockaddr[0])
# Normalize IPv6-mapped IPv4: ::ffff:127.0.0.1 → 127.0.0.1
if isinstance(ip, ipaddress.IPv6Address) and ip.ipv4_mapped:
ip = ip.ipv4_mapped
if (ip.is_private or ip.is_loopback or ip.is_link_local
or ip.is_reserved or ip.is_multicast or ip.is_unspecified):
raise SSRFError(f"Resolved to blocked address: {ip}")
# 4. No automatic redirect following — validate each Location header manually
resp = requests.get(url, timeout=5, allow_redirects=False)
if resp.is_redirect:
raise SSRFError(f"Redirect to {resp.headers.get('location')!r} — not followed")
return resp.textValidating the hostname string alone is insufficient against DNS rebinding. The validate_url() pattern above resolves DNS and checks the IP at validation time. A DNS rebinding attack (CVE-2026-27127 pattern) repoints the hostname between validation and the actual HTTP request. The complete fix: after resolving, connect to the resolved IP directly — do not re-resolve at request time. Use an HTTPAdapter with a custom resolver that pins the resolved IP for the connection, or use httpx with a custom transport that pre-resolves.
const dns = require('dns').promises;
const { URL } = require('url');
const fetch = require('node-fetch');
const ALLOWED_ORIGINS = new Set(['https://api.stripe.com', 'https://hooks.slack.com']);
// Check if IP is private/link-local
function isPrivateAddress(ip) {
return /^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|169\.254\.|127\.|::1$|fc|fd)/i.test(ip);
}
async function safeFetch(rawUrl) {
const parsed = new URL(rawUrl); // throws on invalid URLs
if (!['http:', 'https:'].includes(parsed.protocol)) {
throw new Error(`Disallowed scheme: ${parsed.protocol}`);
}
// Strip trailing dot (CVE-2025-62718 class)
const hostname = parsed.hostname.replace(/\.$/, '');
const origin = `${parsed.protocol}//${hostname}`;
if (!ALLOWED_ORIGINS.has(origin)) {
throw new Error(`Host not in allowlist: ${origin}`);
}
// Resolve and validate all returned IPs
const addresses = await dns.lookup(hostname, { all: true });
for (const { address } of addresses) {
if (isPrivateAddress(address)) {
throw new Error(`Resolved to private address: ${address}`);
}
}
// No redirect following
return fetch(rawUrl, { redirect: 'error', timeout: 5000 });
}import java.net.*;
import java.util.Set;
public class SafeHttpFetcher {
private static final Set<String> ALLOWED_HOSTS = Set.of("api.stripe.com", "hooks.slack.com");
public static InputStream safeFetch(String rawUrl) throws Exception {
URI uri = new URI(rawUrl);
String scheme = uri.getScheme();
// Scheme allowlist
if (!"http".equals(scheme) && !"https".equals(scheme)) {
throw new SecurityException("Only http/https allowed: " + scheme);
}
String host = uri.getHost();
if (!ALLOWED_HOSTS.contains(host)) {
throw new SecurityException("Host not allowed: " + host);
}
// Resolve and check all IPs (IPv6-mapped included)
InetAddress addr = InetAddress.getByName(host);
if (addr.isLoopbackAddress() || addr.isLinkLocalAddress()
|| addr.isSiteLocalAddress() || addr.isAnyLocalAddress()
|| addr.isMulticastAddress()) {
throw new SecurityException("Resolved to private address: " + addr.getHostAddress());
}
HttpURLConnection con = (HttpURLConnection) uri.toURL().openConnection();
con.setInstanceFollowRedirects(false); // no redirect following
con.setConnectTimeout(5000);
con.setReadTimeout(5000);
return con.getInputStream();
}
}# Enforce IMDSv2 on a single instance
aws ec2 modify-instance-metadata-options \
--instance-id i-1234567890abcdef0 \
--http-tokens required \
--http-put-response-hop-limit 1
# Audit: list all instances still using IMDSv1
aws ec2 describe-instances \
--query "Reservations[].Instances[?MetadataOptions.HttpTokens=='optional'].{ID:InstanceId,State:State.Name}" \
--output table
# Organization-wide SCP — deny RunInstances with IMDSv1
# Add to SCP in AWS Organizations{
"Effect": "Deny",
"Action": "ec2:RunInstances",
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringEquals": {
"ec2:MetadataHttpTokens": "optional"
}
}
}Block outbound connections to metadata and private ranges at the infrastructure layer — defense in depth that remains effective even when application-layer validation is bypassed:
# Kubernetes NetworkPolicy — deny egress to metadata and RFC-1918
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-metadata-egress
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 169.254.169.254/32 # AWS/Azure IMDS
- 169.254.170.2/32 # ECS task metadata
- 168.63.129.16/32 # Azure WireServer
- 10.0.0.0/8 # RFC 1918
- 172.16.0.0/12 # RFC 1918
- 192.168.0.0/16 # RFC 1918# Drop CAP_NET_RAW in containers — prevents raw socket protocol smuggling
FROM python:3.12-slim
# ... application setup ...
# In Kubernetes SecurityContext or Docker --cap-drop=NET_RAWpackage main
import (
"fmt"
"net"
"net/http"
"net/url"
"strings"
)
var allowedHosts = map[string]bool{"api.stripe.com": true, "hooks.slack.com": true}
func validateAndFetch(rawURL string) (*http.Response, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, fmt.Errorf("invalid URL: %w", err)
}
if u.Scheme != "http" && u.Scheme != "https" {
return nil, fmt.Errorf("disallowed scheme: %s", u.Scheme)
}
host := strings.TrimSuffix(u.Hostname(), ".") // strip trailing dot
if !allowedHosts[host] {
return nil, fmt.Errorf("host not in allowlist: %s", host)
}
addrs, err := net.LookupHost(host)
if err != nil {
return nil, fmt.Errorf("DNS lookup failed: %w", err)
}
for _, addr := range addrs {
ip := net.ParseIP(addr)
if ip == nil {
continue
}
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsPrivate() || ip.IsUnspecified() {
return nil, fmt.Errorf("resolved to private address: %s", addr)
}
}
// No redirect following
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
return client.Get(rawURL)
}What is Server-Side Request Forgery (SSRF)? SSRF (CWE-918, OWASP A10:2021) is a vulnerability where an attacker causes a server to make HTTP requests to a destination of the attacker's choosing — including internal metadata services, localhost, and RFC-1918 addresses unreachable from the public internet. The server acts as an involuntary proxy using its internal network position.
What is the difference between SSRF and CSRF? CSRF tricks a user's browser into sending a forged request using the victim's session. SSRF tricks the server itself into making a request to an internal resource. SSRF grants access to internal services; CSRF does not. SSRF is a server-side vulnerability; CSRF is a client-side one.
What can an attacker do with SSRF?
Exfiltrate cloud IAM credentials from metadata services, scan internal network topology, reach internal admin panels and databases, achieve RCE via gopher→Redis cron injection, read local files via file://, and pivot to internal Kubernetes/etcd clusters for full secrets extraction.
What is the Capital One SSRF breach?
In 2019, a WAF on AWS EC2 had SSRF. The attacker fetched http://169.254.169.254/latest/meta-data/iam/security-credentials/ISRM-WAF-Role, obtained IAM credentials, and exfiltrated 106 million customer records from S3. Outcome: $80M OCC fine, $190M class action settlement. Root cause: IMDSv1 (no auth) + overprivileged IAM role.
Does IMDSv2 prevent SSRF on AWS?
IMDSv2 blocks single-GET SSRF. Four bypass conditions remain: header-injection SSRF injecting the PUT header, multi-request SSRF, headless Chrome PDF generators executing the full token flow via JavaScript, and redirect chains. Enforce HttpTokens=required via AWS SCP organization-wide as the reliable control.
What is DNS rebinding and how does it bypass SSRF filters?
DNS rebinding exploits the TOCTOU gap between DNS resolution time (validation) and request time. Validation resolves attacker.com → public IP (passes). Attacker repoints DNS (TTL=0) → 169.254.169.254. Server re-resolves at request time and hits the metadata service. Defense: pin the resolved IP and force the connection to that IP directly.
How do you prevent SSRF in Python?
Enforce scheme allowlist, exact hostname allowlist, DNS resolution with socket.getaddrinfo(), IP validation (private/loopback/link-local/reserved/multicast/unspecified all blocked), IPv6-mapped IPv4 normalization, allow_redirects=False, and redirect target re-validation.
What is protocol smuggling in SSRF?
When the server uses a multi-protocol client, schemes beyond http/https reach non-HTTP services: gopher:// → raw TCP to Redis/FastCGI/MySQL; file:// → local filesystem; dict:// → Memcached. gopher://127.0.0.1:6379/ + Redis commands enables cron-based RCE.
What SSRF parameters should I test? webhook, callback_url, notify_url, hook_url, url, redirect, dest, destination, next, return, image_url, avatar_url, thumbnail, import_url, feed_url, remote, download_url. Also JSON body fields: url, endpoint, callback, redirect, image, webhook, jwks_uri, logo_uri.
How do you detect SSRF with Burp Suite? Generate a Burp Collaborator URL and submit it in every URL-accepting parameter and HTTP header (Host, X-Forwarded-Host, Referer). Monitor the Collaborator panel: DNS queries = SSRF exists (HTTP may be blocked); HTTP callback = SSRF confirmed.
What is the CVSS vector for SSRF?
Unauthenticated cloud metadata SSRF: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N = 7.5 High. CVE-2025-6454 (GitLab CRLF→SSRF): CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H = 8.5. CVE-2025-62718 (Axios): 9.3 Critical.
What network controls defend against SSRF?
Block egress to 169.254.0.0/16, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, and 127.0.0.0/8 at the VPC/security-group level. Use Kubernetes NetworkPolicy to deny pod egress to metadata endpoints. Enforce IMDSv2 via AWS SCP.
SSRF (CWE-918, OWASP A10:2021) is a vulnerability where an attacker causes a server to make HTTP requests to an arbitrary destination — including internal services, cloud metadata endpoints, and localhost — that are unreachable from the public internet. The server acts as a proxy, so its internal IP bypasses network controls that would block the same request from an external client.
CSRF tricks a user's browser into sending a forged request to a target site using the victim's session cookie — the attacker cannot read the response. SSRF tricks the server itself into making a request to an internal resource, and the server's response can be returned directly to the attacker. SSRF targets server-to-server trust; CSRF targets browser-to-server trust.
Exfiltrate AWS/GCP/Azure IAM credentials from cloud metadata (169.254.169.254), scan internal network topology via port-timing differences, reach internal admin panels and databases not exposed to the internet, interact with Redis/Memcached/etcd via protocol smuggling (gopher://), read local files via file://, and in some configurations achieve remote code execution via gopher→Redis cron injection.
In 2019, a misconfigured WAF running on AWS EC2 had an SSRF vulnerability. An attacker queried http://169.254.169.254/latest/meta-data/iam/security-credentials/ISRM-WAF-Role, retrieved temporary IAM credentials, and enumerated 700+ S3 buckets. The breach exposed 106 million customer records and resulted in an $80M OCC regulatory fine and a $190M class action settlement — the canonical example of IMDSv1 SSRF impact.
In regular (in-band) SSRF, the server returns the fetched response directly to the attacker. In blind SSRF, the server makes the request but the response is not reflected — the attacker can only confirm exploitation via out-of-band (OOB) channels: DNS resolution callbacks, HTTP hits to an Interactsh/Burp Collaborator server, or timing differentials. Blind SSRF is harder to exploit but harder to detect.
The most reliable SSRF-to-RCE chain uses gopher:// to send raw Redis commands to an internal Redis instance on port 6379. The attacker writes a cron job entry to /var/spool/cron/crontabs/root that spawns a reverse shell, then saves the Redis database to the cron directory. This chain — gopher→FLUSHALL→SET→CONFIG SET dir→CONFIG SET dbfilename→SAVE — is automated by the Gopherus tool.
DNS rebinding exploits the Time-of-Check/Time-of-Use (TOCTOU) gap in SSRF validators. The validator resolves attacker.com → legitimate public IP (passes allowlist), then the attacker changes DNS TTL to 0 and rebinds to 169.254.169.254. When the server makes the actual HTTP request, DNS re-resolution returns the metadata IP. CVE-2026-27127 (Craft CMS) is a production-grade example of this bypass defeating a deployed SSRF patch.
IMDSv2 requires a PUT request with X-aws-ec2-metadata-token-ttl-seconds header to obtain a session token before any metadata GET. This blocks simple single-request SSRF. However, four bypass conditions exist: (1) header-injection SSRF that can inject PUT headers; (2) multi-request SSRF; (3) headless Chrome PDF generators that execute the full IMDSv2 flow via JavaScript fetch(); (4) redirect chains where the redirect server issues the PUT. Enforcing HttpTokens=required via AWS SCP is the reliable control.
Parse the URL with urllib.parse, enforce an explicit scheme allowlist (http/https only), check the hostname against an exact allowlist (not contains/startsWith), resolve DNS with socket.getaddrinfo(), and validate every returned IP with ipaddress.ip_address() — blocking is_private, is_loopback, is_link_local, is_reserved, is_multicast. Set allow_redirects=False and validate each Location header manually. Never use a blocklist — encoding and DNS alias bypasses are too numerous.
When the server fetches URLs using a multi-protocol client (curl, Python urllib, Java URL), schemes beyond http/https can communicate with non-HTTP internal services. gopher:// sends raw TCP bytes to Redis (port 6379) or FastCGI (port 9000). file:// reads local filesystem. dict:// queries Memcached. These schemes convert a read-only SSRF into RCE or credential theft.
Webhook and callback parameters (webhook, callback_url, notify_url, hook_url), URL navigation parameters (url, redirect, dest, destination, next, return), image/media import parameters (image_url, avatar_url, thumbnail, media), and feed/import parameters (import_url, feed_url, remote, download_url). Also test JSON body fields: url, endpoint, callback, redirect, image, webhook, and OAuth fields (jwks_uri, logo_uri).
Use Burp Collaborator: generate a unique subdomain (e.g., abc123.oastify.com) and submit it as the value of every URL-accepting parameter, including HTTP headers (Host, X-Forwarded-Host, Referer). Monitor the Collaborator panel for DNS queries (2 lookups = SSRF exists, HTTP blocked) and HTTP callbacks (full SSRF confirmed). Burp Suite Pro's active scanner also tests for SSRF using Collaborator interactions automatically.
A typical unauthenticated SSRF with cloud metadata exfiltration scores CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N = 7.5 High. CVE-2025-6454 (GitLab CRLF→SSRF) scores CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H = 8.5 High. CVE-2025-62718 (Axios NO_PROXY bypass) scores 9.3 Critical.
Block outbound connections to 169.254.0.0/16 (cloud metadata), 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 (RFC 1918 private), and 127.0.0.0/8 (loopback) at the VPC/security-group level. For AWS, enforce IMDSv2 with HttpTokens=required via Service Control Policy organization-wide. Use Kubernetes NetworkPolicy to restrict pod egress. These network controls remain effective even when application-layer validation is bypassed.