Bypass de /admin mediante falsificación de X-Forwarded-For (s104)
Resumen
Se descubrió y cerró un vector de acceso no autorizado al panel de administración Django (/admin) durante la Auditoría Suprema (sesión 104). Un atacante podía enviar una cabecera falsificada X-Forwarded-For: 127.0.0.1 para hacerse pasar por una conexión local y saltarse la restricción de IP.
Severidad: ALTA · Estado: Cerrado (fix en commit 4340d8e) · Orden en auditoría: 5º y último hallazgo grave del piloto core
El vector
Antes del fix (vulnerable)
El middleware AdminPathsMiddleware tomaba el primer valor de X-Forwarded-For sin validar si la conexión provenía realmente de un proxy de confianza:
forwarded = request.META.get("HTTP_X_FORWARDED_FOR", "")
if forwarded:
ip_str = forwarded.split(",")[0].strip() # ← confianza ciega
else:
ip_str = request.META.get("REMOTE_ADDR", "")
Cualquiera que conectara directamente al puerto :8000 (o a través del proxy Traefik, inyectando un XFF falso) podía enviar:
GET /admin/ HTTP/1.1
X-Forwarded-For: 127.0.0.1
Si 127.0.0.0/8 estaba en ALLOWED_NETWORKS, el bypass triunfaba.
Confirmación empírica (STAGE)
En el entorno STAGE se reprodujo el fallo:
- Acceso legítimo desde IP pública Edu →
302(redirige a login) ✓ - Spoof
X-Forwarded-For: 127.0.0.1desde IP pública →302(bypass exitoso) ✗ - IP pública real sin XFF →
404(como es debido) ✓
La solución (commit 4340d8e)
Lógica nueva: _real_client_ip()
El middleware ahora verifica la confiabilidad del proxy antes de leer X-Forwarded-For:
- Conexión directa (
REMOTE_ADDRno es proxy): Se ignora XFF, se usaREMOTE_ADDRcomo IP real (infalseabilidad nativa de TCP). - Detrás de proxy de confianza (
REMOTE_ADDR∈TRUSTED_PROXY_NETWORKS): Se recorre XFF de derecha a izquierda y se toma el primer hop que NO sea proxy:- Resiste un spoof inicial (p. ej.
127.0.0.1, 37.34.68.35→ se ignora el127.0.0.1y se toma el37.34.68.35) - Robusto ante append/replace de XFF por intermediarios
- Resiste un spoof inicial (p. ej.
Cambios en configuración
ALLOWED_NETWORKS = [
ipaddress.ip_network("37.34.68.35/32"), # Edu — trabajo
ipaddress.ip_network("93.176.0.0/16"), # Edu — casa
ipaddress.ip_network("212.230.190.240/32"),# Edu — nueva sede
ipaddress.ip_network("100.64.0.0/10"), # NetBird VPN (equipo directo)
ipaddress.ip_network("127.0.0.0/8"), # Localhost (health, shell)
]
TRUSTED_PROXY_NETWORKS = [
ipaddress.ip_network("10.0.0.0/8"), # Dokploy network (Traefik)
ipaddress.ip_network("172.16.0.0/12"), # Docker compose bridges
ipaddress.ip_network("127.0.0.0/8"), # localhost proxy hops
]
ALLOWED_NETWORKS: ahora solo contiene IPs de cliente real (públicas de Edu + NetBird del equipo).TRUSTED_PROXY_NETWORKS(nuevo): rangos de proxy de confianza (Docker/Traefik).
Verificación de infra
crearack.comresuelve directo a116.203.31.166(sin Cloudflare) → XFF lleva IP real de Edu como último hop.- Puerto
:8000ya está bloqueado a internet por el firewall Hetzner (confirmado):- STAGE no usa Traefik, así que puede seguir bindear
0.0.0.0:8000sin riesgo. - PROD está cubierto por firewall, así que el binding no se cierra.
- STAGE no usa Traefik, así que puede seguir bindear
- Fix de código cierra el vector real: Traefik + XFF + validación.
Cobertura de tests
Añadido tests/test_admin_paths.py con 9 casos:
- Spoof directo bloqueado (
8.8.8.8+XFF:127.0.0.1) - Spoof vía proxy + IP no permitida bloqueado
- Solo spoof en proxy (XFF inválido) bloqueado
- Acceso legítimo Edu vía Traefik permitido
- Spoof inicial ignorado + IP real permitida (trailing)
- NetBird directo permitido
- Edu público directo permitido
- IP pública aleatoria directa bloqueada
- REMOTE_ADDR malformado bloqueado
Impacto al usuario
- Ninguno: flujo legítimo de Edu (
crearack.com/admin) sigue intacto. - Solo se cierra la puerta trasera (spoof XFF).
Véase también
- [[entity—core—middleware—admin-paths]]
- [[concept—security—multi-tenancy]]
- [[runbook—infra—admin-panel-access]]