CreaRack-SL

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.1 desde 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:

  1. Conexión directa (REMOTE_ADDR no es proxy): Se ignora XFF, se usa REMOTE_ADDR como IP real (infalseabilidad nativa de TCP).
  2. 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 el 127.0.0.1 y se toma el 37.34.68.35)
    • Robusto ante append/replace de XFF por intermediarios

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.com resuelve directo a 116.203.31.166 (sin Cloudflare) → XFF lleva IP real de Edu como último hop.
  • Puerto :8000 ya está bloqueado a internet por el firewall Hetzner (confirmado):
    • STAGE no usa Traefik, así que puede seguir bindear 0.0.0.0:8000 sin riesgo.
    • PROD está cubierto por firewall, así que el binding no se cierra.
  • 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:

  1. Spoof directo bloqueado (8.8.8.8 + XFF:127.0.0.1)
  2. Spoof vía proxy + IP no permitida bloqueado
  3. Solo spoof en proxy (XFF inválido) bloqueado
  4. Acceso legítimo Edu vía Traefik permitido
  5. Spoof inicial ignorado + IP real permitida (trailing)
  6. NetBird directo permitido
  7. Edu público directo permitido
  8. IP pública aleatoria directa bloqueada
  9. 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]]