El SSRF (Server-Side Request Forgery) es una vulnerabilidad que, aunque puede parecer poco frecuente con una tasa de incidencia del 2.72%, resulta altamente peligrosa: con un promedio de 8.28/10 en facilidad de explotación y un impacto significativo de 6.72/10, se han reportado más de 9,500 casos y 385 CVEs (Common Vulnerabilities and Exposures) asociadas, según OWASP.
Estos datos subrayan la urgencia de abordar esta vulnerabilidad en cualquier aplicación web, especialmente por lo fácil que es explotarla y el impacto que puede tener.
Django ofrece una base segura por defecto, pero la seguridad real depende de cómo escribimos nuestro código. En este artículo exploraremos qué es el SSRF, cómo puede infiltrarse en tu proyecto Django, y cómo mitigarlo con buenas prácticas.
¿Qué es el SSRF?
El Server-Side Request Forgery (SSRF), o "falsificación de solicitudes del lado del servidor", es un tipo de vulnerabilidad que ocurre cuando una aplicación web permite a un atacante enviar solicitudes arbitrarias desde el propio servidor hacia otras direcciones, ya sean internas o externas.
Esto convierte al servidor en un puente involuntario, que el atacante puede utilizar para acceder a recursos que normalmente estarían protegidos o serían directamente inaccesibles, como por ejemplo:
- APIs internas que no están expuestas públicamente.
- Bases de datos, servicios de administración o almacenamiento interno.
- Endpoints especiales que los proveedores cloud usan para entregar credenciales o configuraciones.
- Direcciones IP reservadas como
127.0.0.1
,localhost
,169.254.169.254
, etc.
Condiciones para que un ataque sea clasificado como SSRF.
- El usuario puede controlar una URL o parte de ella.
- El servidor realiza una solicitud saliente basándose en esa entrada.
El backend ejecuta un
GET
,POST
,HEAD
, etc., usando el valor proporcionado. - El servidor puede acceder a recursos que el atacante no podría alcanzar directamente. Recursos protegidos por firewall, direcciones internas, puertos cerrados, etc.
- La respuesta de la solicitud puede ser aprovechada por el atacante. Ya sea para leer datos, escanear infraestructura, ejecutar código o filtrar información.
Un pequeño diagrama de ejemplo:
SSRF en el desarrollo con Django.
En Django, el SSRF suele surgir cuando usamos librerías como requests
para consumir recursos externos basándonos en URLs que el usuario puede enviar libremente, sin verificación alguna.
Ejemplo típico:
import requests
def fetch_url(request):
url = request.GET.get('url')
response = requests.get(url)
return HttpResponse(response.text)
Este código es funcional, pero si no validamos el destino, se vuelve un vector de ataque perfecto.
Ejemplos comunes de ataques SSRF.
1. Acceso a Recursos Internos.
Ejemplo vulnerable:
def fetch_url(request):
url = request.GET.get('url')
response = requests.get(url)
return HttpResponse(response.text)
Solución segura:
from urllib.parse import urlparse
from django.http import HttpResponseBadRequest
ALLOWED_DOMAINS = ['api.example.com']
def is_safe_url(url):
try:
parsed = urlparse(url)
return parsed.scheme in ['http', 'https'] and parsed.hostname in ALLOWED_DOMAINS
except Exception:
return False
def safe_fetch_url(request):
url = request.GET.get('url')
if not is_safe_url(url):
return HttpResponseBadRequest("URL no permitida")
response = requests.get(url, timeout=5)
return HttpResponse(response.text)
2. Escaneo de Puertos.
Ejemplo vulnerable:
def scan(request):
host = request.GET.get('host')
res = requests.get(f"http://{host}")
return HttpResponse(res.text)
Solución segura:
import re
BLOCKED_PATTERNS = [
r'^127\.', r'^192\.168\.', r'^10\.', r'^172\.(1[6-9]|2\d|3[0-1])\.',
r'^169\.254\.', r'^::1$', r'^localhost'
]
def is_safe_host(host):
for pattern in BLOCKED_PATTERNS:
if re.match(pattern, host):
return False
return True
def secure_scan(request):
host = request.GET.get('host')
if not is_safe_host(host):
return HttpResponseBadRequest("Host no permitido.")
try:
res = requests.get(f"http://{host}", timeout=3)
return HttpResponse(res.text)
except:
return HttpResponse("Error en la conexión.")
3. Inclusión Remota de Archivos (RFI).
Ejemplo vulnerable:
def load_template(request):
url = request.GET.get('template')
template_code = requests.get(url).text
return render(request, template_code)
Solución segura:
from django.template.loader import render_to_string
ALLOWED_TEMPLATES = ['template1.html', 'template2.html']
def load_local_template(request):
template_name = request.GET.get('template')
if template_name not in ALLOWED_TEMPLATES:
return HttpResponseBadRequest("Plantilla no permitida.")
html = render_to_string(template_name)
return HttpResponse(html)
4. Exfiltración de Datos.
Ejemplo vulnerable:
def get_internal_api_data(request):
api_url = request.GET.get('url')
res = requests.get(api_url)
return JsonResponse(res.json())
Solución segura:
import os
INTERNAL_API = os.environ.get('INTERNAL_API_URL', 'http://internal-api.local/data')
def secure_api_data(request):
headers = {'Authorization': 'Token xyz...'}
response = requests.get(INTERNAL_API, headers=headers, timeout=5)
return JsonResponse(response.json())
5. Ataques XXE (XML External Entity).
Ejemplo vulnerable:
import xml.etree.ElementTree as ET
def parse_xml(request):
xml_data = request.body
tree = ET.fromstring(xml_data)
return HttpResponse("OK")
Solución segura:
from defusedxml.ElementTree import fromstring
def safe_parse_xml(request):
xml_data = request.body
try:
tree = fromstring(xml_data)
return HttpResponse("XML procesado con seguridad")
except Exception:
return HttpResponseBadRequest("XML inválido.")
6. Server-Side Cache Poisoning.
Ejemplo vulnerable:
@cache_page(60 * 15)
def show_data(request):
user_id = request.GET.get('user')
return HttpResponse(f"Datos para el usuario {user_id}")
Solución segura:
from django.views.decorators.cache import cache_page
from django.utils.decorators import method_decorator
@method_decorator(cache_page(60 * 15, key_prefix="user_data"), name='dispatch')
def secure_show_data(request):
if not request.user.is_authenticated:
return HttpResponseForbidden("No autenticado.")
return HttpResponse(f"Datos del usuario: {request.user.username}")
7. Abuso de la API de Metadatos en la Nube.
Ejemplo vulnerable:
def fetch_metadata(request):
url = request.GET.get('url')
response = requests.get(url)
return HttpResponse(response.text)
Solución segura:
from urllib.parse import urlparse
BLOCKED_IPS = ['169.254.169.254', 'metadata.google.internal']
def is_metadata_access(url):
parsed = urlparse(url)
return parsed.hostname in BLOCKED_IPS
def secure_metadata_fetch(request):
url = request.GET.get('url')
if is_metadata_access(url):
return HttpResponseBadRequest("Acceso a metadatos bloqueado.")
res = requests.get(url, timeout=3)
return HttpResponse(res.text)
Buenas prácticas y consideraciones adicionales.
No he querido conformarme con solo señalar las buenas practicas para el desarrollo seguro y la protección contra ataques SSRF, por lo que preferí sacrificar la brevedad del post por un enfoque más completo sobre el desarrollo seguro.
- Usa listas positivas (whitelists)
En lugar de intentar bloquear todas las direcciones o dominios maliciosos (lo cual es casi imposible), es mucho más seguro definir explícitamente una lista de destinos válidos a los que tu aplicación tiene permitido hacer solicitudes.
- Valida entradas con
urlparse()
Python ofrece el módulo urllib.parse
, que incluye la función urlparse()
, ideal para analizar una URL y descomponerla en partes (esquema, host, puerto, ruta, etc.).
Esto permite validar:
- Que la URL use un esquema aceptable (como
http
ohttps
). - Que el hostname esté en la lista permitida.
- Que no sea una IP local o maliciosa.
Ejemplo básico:
from urllib.parse import urlparse
def is_valid_url(url):
parsed = urlparse(url)
return parsed.scheme in ['http', 'https'] and parsed.hostname in ALLOWED_DOMAINS
Esta validación es sencilla pero efectiva para prevenir que un atacante use direcciones como 127.0.0.1
o localhost
.
- Establece
timeout
en tus solicitudes externas
Cuando haces solicitudes HTTP con librerías como requests
, es importante establecer un tiempo máximo de espera (timeout
). De lo contrario, tu servidor podría quedar bloqueado si una solicitud tarda demasiado o nunca responde, lo que podría usarse para un ataque de denegación de servicio (DoS).
Ejemplo:
response = requests.get(url, timeout=5)
Esto le dice a requests
que espere como máximo 5 segundos antes de cancelar la solicitud.
- Implementa
django-ratelimit
para evitar abuso
La librería django-ratelimit
permite limitar cuántas veces se puede acceder a una vista desde una IP, usuario o parámetro específico, en un intervalo de tiempo.
Esto es útil para:
- Evitar que un atacante abuse de un endpoint vulnerable.
- Mitigar intentos automatizados de escaneo o exfiltración.
Ejemplo básico:
from django_ratelimit.decorators import ratelimit
@ratelimit(key='ip', rate='5/m', block=True)
def fetch_url(request):
# ...
Este ejemplo limita el acceso a 5 solicitudes por minuto desde la misma IP.
- Loguea y monitorea los endpoints sensibles
Es fundamental registrar (loggear) todas las solicitudes hechas a endpoints que permiten interactuar con recursos externos. Esto no solo ayuda a detectar intentos de explotación, sino que puede ser clave para auditorías de seguridad o análisis post-incidente.
Consejo: guarda al menos el timestamp, IP de origen, URL solicitada y el resultado.
- Segmenta la red y limita la comunicación saliente
A pesar de que en este post nos centramos en el desarrollo con Django, no todo se limita al nivel del framework. A nivel de infraestructura, es buena práctica que los servidores donde se ejecuta tu aplicación no tengan acceso irrestricto a toda la red, ni puedan hacer solicitudes salientes libremente.
Recomendaciones:
- Configura firewalls o reglas de seguridad en la nube para limitar el tráfico saliente.
- Usa redes separadas (VPCs o subredes) para servicios sensibles como bases de datos o APIs internas.
- Desactiva el acceso a endpoints de metadatos si no es necesario (especialmente en servicios cloud).
Conclusión.
El SSRF es una amenaza silenciosa pero crítica. Si bien Django ofrece una base sólida de seguridad, las decisiones que tomamos como desarrolladores al procesar entradas del usuario son fundamentales. Validar, restringir y monitorear las URLs que la app puede solicitar es esencial para prevenir este tipo de ataques.
Fuentes.
- OWASP: Server-Side Request Forgery
- Spectral: 7 Examples of SSRF
- PortSwigger: SSRF Cheatsheet
- Snyk: SSRF Tutorial