Server fetches an attacker-supplied URL, exposing internal services, cloud metadata endpoints, or localhost resources.
TL;DR
url, webhook, callback, image, redirect, srcAccessKeyId, SecretAccessKey, TokenBasic (in-band) SSRF is the simplest and most immediately impactful form of CWE-918. An application accepts a URL from user input, makes a server-side HTTP request to that URL, and returns the response — or a portion of it — directly in the HTTP reply. The attacker sees the response from the internal target as if the server is a transparent proxy.
The distinguishing characteristic is visibility: unlike blind SSRF, where the response is suppressed, basic SSRF returns the retrieved content. This makes it trivially exploitable for cloud credential theft and internal reconnaissance without needing out-of-band infrastructure. OWASP A10:2021 identifies this pattern as one of the highest-impact web vulnerabilities precisely because the cloud metadata endpoint (169.254.169.254) is reachable by default from any AWS, GCP, or Azure instance.
The server accepts user input containing a URL, constructs an HTTP request, and reflects the response. The attack leverages the server's trusted network position: internal services that would return 403 Forbidden or be unreachable from the internet respond normally to requests from the server's IP.
The attack chain:
url, src, image, callback, webhook, redirect, import, etc.)http://127.0.0.1/, http://169.254.169.254/, http://192.168.1.1:8080/admin/)The vulnerable code pattern in Python:
# VULNERABLE — direct user-controlled URL fetch
import requests
def fetch_preview(url: str) -> dict:
# url = "http://169.254.169.254/latest/meta-data/iam/security-credentials/ROLE"
resp = requests.get(url, timeout=10, allow_redirects=True)
return {"content": resp.text, "status": resp.status_code}
# Attacker receives IAM credentials in JSON response| Technique | Target | Example Payload | Impact |
|---|---|---|---|
| Cloud metadata — IMDSv1 | AWS 169.254.169.254 | http://169.254.169.254/latest/meta-data/iam/security-credentials/ | IAM credential theft |
| Cloud metadata — GCP | metadata.google.internal | http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token | GCP service account token |
| Localhost admin panel | Internal admin UI | http://127.0.0.1:8080/admin/ | Admin access bypass |
| Internal service scan | Redis, Elasticsearch | http://127.0.0.1:9200/_cat/indices | Data exfiltration |
| Internal API | Microservice | http://internal-api.svc.cluster.local/v1/users | Business data access |
| Link unfurling | OG/meta scraper | http://169.254.169.254/ | Cloud credentials via chat app |
Link unfurling is a high-value hidden attack surface: applications that generate URL previews (Slack, Discord, CMS preview cards) all make server-side requests to attacker-supplied URLs.
Capital One 2019 — The canonical SSRF breach. A WAF product deployed on an EC2 instance had a misconfiguration that allowed SSRF. The attacker submitted HTTP requests containing the AWS IMDSv1 metadata URL. The WAF made the request from its internal EC2 IP, returned the IAM role name, then the attacker fetched the credential JSON: {"AccessKeyId":"ASIAJWNJSXFH...","SecretAccessKey":"...","Token":"...","Expiration":"2019-03-18T12:54:10Z"}. The overprivileged ISRM-WAF-Role had S3 GetObject access on 700+ buckets. Approximately 30 GB of data was exfiltrated — 100 million US records and 6 million Canadian records. The resulting $80M OCC fine was the largest ever for a data breach at the time. At disclosure (2024), only 32% of EC2 instances had IMDSv2 enforced.
CVE-2024-8977 — GitLab EE (CVSS 8.2, CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:N) — GitLab's Product Analytics Dashboard made server-side HTTP requests to configurable Cube API endpoints. Any authenticated user could replace the Cube endpoint URL with http://169.254.169.254/latest/meta-data/ and the dashboard would display the raw internal response. Fixed in GitLab 17.4.2, 17.3.5, 17.2.9 (October 2024). Reported via HackerOne by joaxcar.
CVE-2025-68437 — Craft CMS — The saveAssets GraphQL mutation accepted arbitrary URLs in _file { url } and fetched them server-side. The fetched content was saved as an asset and downloadable via the CMS API — turning a standard upload feature into a full-read in-band SSRF. CMS platforms that allow asset import from URL are a consistently underestimated attack surface.
Webhook "test" buttons are a frequently overlooked SSRF vector. When a platform lets you test a webhook URL by making a server-side request and displaying the response, that is an in-band SSRF. HackerOne report #2301565 earned $2,500 for exactly this pattern.
url, src, href, callback, webhook, endpoint, redirect, image, avatar, import_url, feed, resource), and HTTP headers (Host, X-Forwarded-Host, Referer).http://127.0.0.1/ and compare the response to a control request with a legitimate URL. A difference in content, length, or status code confirms the server made an outbound request.http://169.254.169.254/latest/meta-data/ — if the response contains ami-id, instance-id, or IAM role data, the application is in a cloud environment and credentials are directly accessible.http://127.0.0.1:8080/admin/, http://localhost:3000/, http://127.0.0.1:9200/ (Elasticsearch).# Burp Repeater — basic SSRF test
GET /api/image-preview?url=http://169.254.169.254/latest/meta-data/ HTTP/1.1
Host: target.example.com
# Expected vulnerable response:
# ami-id
# ami-launch-index
# hostname
# iam/
# ... (metadata listing)Nuclei includes SSRF templates that test common parameter names against cloud metadata endpoints. SSRFmap automates multi-parameter fuzzing with cloud-specific payloads. Burp Scanner Pro identifies URL-accepting parameters and tests OOB callbacks via Collaborator.
BreachVex scans 80+ canonical SSRF parameter names plus value-based detection (any parameter value matching https?:// or bare IPv4), fires cloud metadata probes for AWS/GCP/Azure, and measures differential response against RFC 5737 control IPs (192.0.2.1) to confirm request execution without requiring response reflection.
import ipaddress
import socket
import urllib.parse
from typing import Optional
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)
# Scheme allowlist — rejects gopher://, file://, dict://, ftp://
if parsed.scheme not in ("http", "https"):
raise SSRFError(f"Disallowed scheme: {parsed.scheme}")
host = (parsed.hostname or "").rstrip(".") # strips trailing FQDN dot bypass
if not host:
raise SSRFError("Missing hostname")
# Exact host allowlist — not contains/startsWith
if host not in ALLOWED_HOSTS:
raise SSRFError(f"Host not in allowlist: {host}")
# Resolve DNS and validate ALL returned addresses (defeat DNS aliases + nip.io)
try:
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 # normalize ::ffff:169.254.169.254
if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
raise SSRFError(f"Resolved to blocked address: {ip}")
except (socket.gaierror, ValueError) as e:
raise SSRFError(f"DNS validation failed: {e}")
import requests
# Disable redirect following — validate Location header separately
resp = requests.get(url, timeout=5, allow_redirects=False)
if resp.is_redirect:
raise SSRFError(f"Redirect to unvalidated URL: {resp.headers.get('location')}")
return resp.textDo not return raw internal responses. If the feature only needs a title or image, extract the specific field and discard the rest:
# VULNERABLE — returns raw internal response
return {"preview": requests.get(url).text}
# SAFE — extracts only the needed field
from bs4 import BeautifulSoup
soup = BeautifulSoup(requests.get(validated_url, allow_redirects=False).text, "html.parser")
title = soup.find("title")
return {"preview_title": title.text[:200] if title else ""}DNS resolution validation must happen at request time, not at registration time. An attacker can register trusted.example.com on your allowlist when it resolves to a legitimate IP, then change the DNS record to 169.254.169.254. Validate the resolved IP for every request — this is called DNS pinning.
Any parameter whose value is used in a server-side HTTP request is a candidate: url, src, href, redirect, callback, webhook, image, avatar, import_url, feed, endpoint, and 80+ canonical names. Value-based detection catches unlabeled fields: any parameter whose value matches http://, //, or a bare IPv4 pattern is a SSRF candidate regardless of name.
In basic (in-band) SSRF, the server returns the response body to the attacker — the internal resource content is visible in the HTTP reply. In blind SSRF, the server makes the request but suppresses the response; confirmation requires an out-of-band callback (Burp Collaborator, Interactsh DNS/HTTP hit).
Any service reachable from the server's network: cloud metadata endpoints (169.254.169.254), admin panels, Redis on 6379, Elasticsearch on 9200, Kubernetes API on 6443, Docker API on 2375, etcd on 2379, internal monitoring dashboards, and any microservice on the internal network not exposed to the internet.
Step 1: fetch http://169.254.169.254/latest/meta-data/iam/security-credentials/ to get the IAM role name. Step 2: fetch http://169.254.169.254/latest/meta-data/iam/security-credentials/{ROLE_NAME} to get AccessKeyId, SecretAccessKey, Token. Step 3: use the credentials with AWS CLI to enumerate S3 buckets, EC2 instances, or other resources.
In 2019, a misconfigured WAF on EC2 had a basic SSRF vulnerability. The attacker queried 169.254.169.254 directly, obtained IAM credentials for the overprivileged role ISRM-WAF-Role, and used them to access 700+ S3 buckets. The breach exposed 106 million records and resulted in an $80M OCC fine. The response was visible in-band — classic basic SSRF.
GitLab EE's Product Analytics Dashboard made server-side requests to configurable Cube API endpoints. Authenticated users could replace the endpoint URL with internal addresses. The response was returned to the UI, making it full in-band SSRF. CVSS 8.2, fixed in GitLab 17.4.2.
Rather than matching parameter names, value-based detection identifies parameters whose current value already contains a URL pattern (https?://, //, or bare IPv4). This catches cases where the parameter is named 'data', 'config', or 'settings' but the value is 'http://internal:9200'. BreachVex uses both name-based and value-based detection.
Maintain an allowlist of permitted destinations (exact domain match, not startsWith or endsWith). Resolve DNS for each allowed domain and reject if it resolves to a private/link-local/loopback IP. Disable redirect following — validate each Location header against the same allowlist. Log all outbound requests with destination and response status for anomaly detection.