Server-Side Request Forgery (SSRF) is what happens when a web application fetches URLs on behalf of a user, and the user gets to pick the URL. The application becomes a proxy the attacker can steer at anything the server has network access to: internal services, cloud metadata endpoints, database ports, admin interfaces that would otherwise be firewalled off from the internet.
SSRF got its own OWASP Top 10 category in 2021 (A10:2021), added largely on the strength of the community survey rather than the data. It earned the spot by showing up constantly in cloud-native architectures and doing enormous damage when it lands. The Capital One 2019 breach, reportedly kicked off by an SSRF bug, ended with about 106 million customer records gone.
The mechanics#
SSRF vulnerabilities live wherever a server-side component fetches a URL that the user controls. Common patterns:
- Image or file preview features (fetch this URL, generate a thumbnail)
- Webhook configuration (POST notifications to this URL)
- URL-to-PDF or URL-to-image conversion services
- OAuth callback URLs (though usually with allowlist enforcement)
- Server-side embed rendering (fetch this OpenGraph URL, extract metadata)
- Import-from-URL features (import this JSON from that URL)
If the fetch is unrestricted, the attacker can point it at anywhere the server can reach on the network:
- The server itself (localhost, 127.0.0.1)
- Other services on the same internal network (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
- Link-local addresses (169.254.0.0/16), which include cloud metadata endpoints
- Cross-cloud metadata endpoints (169.254.169.254 on AWS/Azure/GCP; different for each)
The attacker doesn’t need the response body to be returned to them. Even blind SSRF (where the server fetches but doesn’t relay the response) can:
- Enumerate internal services via response timing or error differences
- Trigger state changes on internal APIs (POST /admin/users)
- Exfiltrate data via DNS lookups (the target’s outbound DNS resolver is the exfiltration channel)
- Steal cloud IAM credentials via metadata endpoints and use them elsewhere
Cloud metadata: the SSRF crown jewel#
Cloud providers expose an HTTP metadata service to running instances that reveals configuration, credentials, and secrets. This service is designed to be reachable only from the instance itself, over a link-local address: 169.254.169.254 on AWS and Azure, and the same IP on GCP as an alias for its canonical metadata.google.internal hostname. SSRF-to-metadata is the specific chain that made Capital One 2019 catastrophic.
AWS IMDS#
The AWS Instance Metadata Service (IMDS) exposes IAM role credentials, user data, and instance identity at http://169.254.169.254/latest/meta-data/. IMDSv1 (the original) responds to any HTTP GET; SSRF a GET to that URL and you get back the credentials of whatever IAM role is attached to the instance. Then you use those credentials against AWS APIs from your own machine.
IMDSv2 (released November 2019, partly in response to Capital One) requires a session token obtained via a PUT request with a specific header. This is designed specifically to prevent SSRF-style access:
# IMDSv1 (still available but discouraged, and vulnerable to SSRF)
GET http://169.254.169.254/latest/meta-data/iam/security-credentials/rolename
# IMDSv2 (session-authenticated, resistant to naive SSRF)
PUT http://169.254.169.254/latest/api/token
X-aws-ec2-metadata-token-ttl-seconds: 21600
(returns a token)
GET http://169.254.169.254/latest/meta-data/iam/security-credentials/rolename
X-aws-ec2-metadata-token: <token>Most SSRF bugs are HTTP GET-based and can’t emit the PUT that IMDSv2 token acquisition needs, and fewer still can attach a custom header to that PUT. AWS has pushed IMDSv2 toward the default slowly: Amazon Linux 2023 enforces it, console Quick Start launches defaulted to it in late 2023, an account-level “IMDSv2 by default for new launches” setting arrived in March 2024, and instance types released since mid-2024 ship IMDSv2-only. Older instance types still allow both versions unless you set that account default or bake it into the AMI, which is why the guidance is to enforce IMDSv2-only across the fleet rather than assume you’re covered.
GCP metadata service#
Google Cloud’s metadata endpoint requires a Metadata-Flavor: Google header on every request. This blocks naive SSRF that can only issue plain GET requests, though SSRF vulnerabilities that let the attacker set request headers can bypass it.
Azure IMDS#
Azure requires a Metadata: true header and additionally rejects any request that includes an X-Forwarded-For header (a signal the request has been proxied rather than issued directly by the instance). Naive SSRF is blocked; header-controlling SSRF can bypass if it can add the right header and avoid the wrong one.
For all three clouds, the pattern is the same: don’t rely on the header requirement as the only defense. Block outbound access to the metadata address from anywhere it shouldn’t be reachable, use v2/header-enforced modes, and scope IAM roles narrowly so a stolen credential has limited blast radius.
Bypassing naive filters#
Applications that “block internal IPs” or “allowlist known-good hosts” often fail because URL parsing is harder than it looks. The classic bypasses:
IP address encoding tricks#
127.0.0.1 can be written as:
127.1(implicit octet expansion)2130706433(decimal 32-bit integer)0x7f000001(hex)017700000001(octal)[::1]or[::ffff:127.0.0.1](IPv6 loopback / IPv4-mapped)127.0.0.1.nip.io(public DNS that resolves to 127.0.0.1)
Any URL parser that only string-matches for “127” or “localhost” will miss most of these.
URL parser confusion#
Different URL parsers interpret the same string differently. A URL like http://allowed-host@internal-host/ might be parsed as:
- “the host is
internal-host, the userinfo isallowed-host” (correct per RFC 3986) - “the host is
allowed-host” (incorrect but common)
If your validator uses one parser and your HTTP client uses another, and they disagree about which is the host, you have a bypass. The PayloadsAllTheThings SSRF page catalogs dozens of these.
DNS rebinding#
Time-of-check to time-of-use attacks against DNS-based allowlists. The attacker registers a domain whose DNS record initially resolves to a public, allowed IP but then rotates to an internal IP with a low TTL. The application checks the initial resolution, decides the host is safe, then makes the actual HTTP request which re-resolves and gets the internal IP.
Defense: resolve DNS once, cache the resolution, use the resolved IP directly for the actual request. Don’t allow the second lookup to return a different result than the validation lookup did.
URL scheme abuse#
Beyond HTTP, other URL schemes can be more dangerous:
file://reads local files (file:///etc/passwd)gopher://lets the attacker construct arbitrary bytes to send to a TCP port (weaponized against Redis, Memcached, MySQL, SMTP)dict://can be used similarly for line-based protocolsldap://,ftp://,jar://(Java) each open specific attack classesredis://if the HTTP client library happens to support it
gopher:// is the standout because it lets you craft arbitrary bytes over TCP, which turns any SSRF into a general-purpose attack against unauthenticated internal services. Unauthenticated Redis on 127.0.0.1:6379 becomes remote command execution via CONFIG SET and SAVE sequences.
Application HTTP clients should explicitly restrict to http and https schemes and reject everything else.
Notable SSRF incidents#
Capital One (July 2019) is the reference case. A misconfigured WAF running on an EC2 instance (widely reported in security research as a ModSecurity-based reverse proxy) reportedly carried an SSRF flaw that let an attacker pull IMDSv1 credentials for the WAF’s IAM role. The role was over-permissioned for S3, and Capital One’s data sat in buckets it could read. Around 106 million customer records exfiltrated. Capital One took an $80 million OCC fine and a $190 million class-action settlement. The incident directly drove AWS’s development and promotion of IMDSv2, released a few months later.
Google Cloud internal-service access (Pereira). Researcher Ezequiel Pereira has published a string of findings that reach Google’s internal services from the outside: a 2018 App Engine bug that hit internal Google APIs, and the Deployment Manager flaw that won the 2020 GCP VRP prize (announced 2021) by issuing requests to internal services as a privileged service account. Different mechanisms than classic metadata SSRF, same payoff, and his own writeups are the primary references.
GitLab CVE-2021-22214 (June 2021). Unauthenticated SSRF via the CI Lint API’s include feature. GitLab servers could be induced to fetch attacker-controlled URLs, which combined with the metadata service reachability made it a serious cloud-hosted-GitLab problem.
WebLogic CVE-2014-4210. An SSRF via the WebLogic UDDI Explorer (uddiexplorer/SearchPublicRegistries.jsp). Not truly blind: its error responses differ enough to fingerprint open ports, which makes it a usable internal port scanner. It still turns up in appsec assessments a decade later because unpatched WebLogic persists. It was later reused as a metadata-endpoint pivot against cloud-hosted Weblogic instances.
Kubernetes API SSRF. In-cluster applications with SSRF can often reach the Kubernetes API server at its well-known internal name (kubernetes.default.svc). Reaching it is not the same as querying it: a plain GET-only SSRF can’t attach the service account bearer token, so the dangerous case is an SSRF that can set the Authorization header, or an application that already presents the token itself. This is the Kubernetes-native equivalent of the AWS IMDS pattern: an SSRF in a workload can enumerate secrets, service accounts, and other pods if the workload’s service account has any meaningful RBAC binding. Restrict service account permissions and set automountServiceAccountToken: false on pods that don’t need API access.
Detection#
Blind SSRF (where the response never gets back to the attacker) needs out-of-band detection. Two standard tools:
- Burp Collaborator is PortSwigger’s out-of-band service. Inject Collaborator payloads into URL parameters, watch for DNS lookups or HTTP requests back to the Collaborator server, which proves SSRF even when the application discards the response.
- interactsh from ProjectDiscovery is the open-source equivalent, self-hostable or usable via the public interact.sh service.
For automated scanning:
- SSRFmap (Swissky) is a Python-based SSRF exploitation framework. Give it a request template and it will try dozens of payloads and encodings, target common internal services, and attempt to escalate to full compromise. Still actively maintained.
- Nuclei (ProjectDiscovery) includes SSRF detection templates in its public template library. Fast and integrates well with CI/CD pipelines.
- Burp Suite Professional provides the Repeater and Intruder tools that most manual SSRF hunting still happens in, plus the SSRF-focused extensions in the BApp store.
For code review, grep for the patterns that fetch URLs: requests.get(url), urllib.request.urlopen(url), HttpClient.GetAsync(url), curl_exec($ch), fetch(url) on the server side. Every hit needs to be checked for URL validation and allowlist enforcement.
Defenses that actually work#
Ordered by effectiveness in practice:
Use IMDSv2 (or the equivalent on other clouds) and set it to required. This alone would have prevented Capital One and continues to prevent the crown-jewel SSRF-to-cloud-credentials scenario. It’s the single highest-value control for cloud-hosted applications.
Egress firewall rules. Block outbound traffic from application servers to RFC 1918 addresses, link-local (169.254/16), and any other network you don’t intend the application to reach. This is defense in depth: even if the application can be tricked into making a request, the firewall stops the request from reaching the target.
Strict allowlists at the application layer. If your application needs to fetch user-supplied URLs, allow only specific hosts. github.com/user/repo for a specific integration is safer than “any HTTPS URL.” Allowlists must operate on the resolved IP after DNS lookup, not just the hostname, to prevent DNS rebinding.
DNS pinning. Resolve the URL’s hostname once, validate the resolved IP against your allowlist, then make the HTTP request to that specific IP with a Host header set to the original hostname. This defeats DNS rebinding: the actual request goes to the IP you validated, not to whatever DNS returns on the second lookup.
Restrict URL schemes. Reject anything that isn’t http or https before it reaches the fetching code. If your app has legitimate reason to use other schemes, be explicit about which.
Least-privilege IAM. If the SSRF does succeed and does steal credentials, the credentials should let the attacker do as little as possible. Instance roles with s3:* and iam:ListRoles shouldn’t exist; the role should have only what the specific application actually needs.
Monitor and log. Application-level logging of all outbound fetches, including the URL and the resolved IP. Aggregate to a SIEM and alert on requests to metadata endpoints, private ranges, or unusual hosts. This is the “we got hit but we noticed” layer.
Rate limit outbound requests. Prevents SSRF from being weaponized for internal port scanning or resource exhaustion.
Further reading#
- PortSwigger Web Security Academy: SSRF - hands-on labs, still the best free training resource
- OWASP Server Side Request Forgery Prevention Cheat Sheet - defensive reference
- HackTricks SSRF page - bypass technique catalog
- PayloadsAllTheThings SSRF - payload reference
- Orange Tsai’s “A New Era of SSRF” - Black Hat 2017 talk on URL parser exploitation
SSRF punishes the assumption that an internal network is safe because it isn’t reachable from the internet. In a cloud environment, where that same internal network also hands out the credentials to everything else, the cost of the assumption climbs fast. Fixing it properly takes input validation, network policy, and least-privilege IAM working together, because any one of them on its own is easy to slip past.