
Reprodução offline e inofensiva da CVE-2026-93355 — tomada de conta via JWT de e-mail não verificado no LiteLLM (executa o código real do LiteLLM)
Um laboratório autocontido e descartável que reproduz
CVE-2026-93355: a autenticação
JWT do LiteLLM resolve a identidade do
chamador recorrendo a uma correspondência por e-mail — e nunca verifica a
claim email_verified. Um atacante que consiga fazer com que a sua própria
conta no provedor de identidade emita um token devidamente assinado contendo o
endereço de e-mail da vítima (não verificado) herda o papel da vítima, até
proxy_admin, e a correspondência reescreve silenciosamente a associação SSO
armazenada da vítima para o subject do atacante, tornando a tomada de conta
persistente.
| Advisory | VulnCheck · OX Security |
| CVE | CVE-2026-93355 |
| Projeto | litellm (proxy / autenticação JWT) |
| Afetado | ≤ 1.102.1 segundo o advisory — ainda sem correção: o código vulnerável permanece inalterado em 1.104.0 (última versão estável) e em main a partir de 2026-10-07 |
| CVSS | 8.1 HIGH (AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N) |
| Fraqueza | CWE-1390 — autenticação fraca |
| Causa raiz | Pesquisa de utilizador por e-mail confiada sem verificar email_verified |
| Descoberta | Nir Zadok & Moshe Siman Tov Bustan (OX Security) |
Estado da correção (2026-10-07): ainda não existe uma versão corrigida. O advisory lista apenas
1.102.1porque era a versão mais recente no momento da divulgação; desde então o LiteLLM lançou1.103.xe1.104.0, e_get_fuzzy_user_objectem ambas — e emmain— continua sem qualquer verificação deemail_verified. O laboratório fixa1.102.1por reprodutibilidade;scripts/verify_against_real_source.pysinalizará o dia em que uma correção for lançada.
Isto reproduz uma vulnerabilidade divulgada publicamente para fins educativos e defensivos. Corre inteiramente em local e não toca em nenhum serviço real:
[email protected] dentro deste processo e imprime-a. Não existe conta real,
chave real, e nada sai da sua máquina.O proxy do LiteLLM permite autenticar com um JWT do seu provedor de identidade
configurado. Para mapear esse token a um utilizador do LiteLLM, chama
get_user_object(), que tenta o subject do token (sso_user_id) e, em caso de
falha, recorre a _get_fuzzy_user_object() — uma correspondência sem
distinção de maiúsculas/minúsculas na claim email. Quem for dono desse e-mail
na tabela do LiteLLM é quem você se torna. A claim email_verified do token
nunca é lida — nem durante a extração de claims
(JWTHandler.get_user_email) nem durante a resolução. Assim, qualquer IdP que
emita um token para um e-mail não verificado (a maioria fá-lo, por conceção — a
verificação é responsabilidade da relying party) permite a um atacante definir o
e-mail do seu perfil como o da vítima, apresentar o token e herdar o papel da
vítima. Pior ainda, numa correspondência por e-mail o LiteLLM dispara uma tarefa
em segundo plano que substitui o sso_user_id da vítima pelo subject do
atacante — pelo que todos os logins posteriores resolvem diretamente para a
conta da vítima sem necessidade de e-mail, e alterar o e-mail não ajuda.

Tudo o que está sob teste é código do próprio LiteLLM, importado do pacote
fixado litellm==1.102.1. O laboratório fornece apenas as duas coisas
necessárias: uma tabela de utilizadores falsa em memória e um par de chaves de
IdP mock. Não reimplementa a lógica vulnerável.
inspect.getsource() na função instalada
_get_fuzzy_user_object e JWTHandler.get_user_email e verificamos que o
fallback de email existe, o rebind de sso_user_id existe, e nenhuma das funções
referencia email_verified. → scripts/verify_against_real_source.py_get_fuzzy_user_object real; ele
resolve para a linha proxy_admin da vítima. → poc/01_account_takeover.pysso_user_id da vítima ao subject do atacante; um login posterior sem nenhum email
resolve diretamente para admin, e "corrigir o email duplicado" não o revoga.
→ poc/02_persistent_rebind.pyemail_verified antes do fallback de email bloqueia o
ataque, mantendo os logins SSO verificados funcionando. → poc/03_patched_path.py./run.sh
run.sh cria um .venv descartável, instala litellm[proxy]==1.102.1, executa a
verificação de referência e, em seguida, todas as três PoCs. A primeira execução baixa o LiteLLM e leva
alguns minutos; exclua .venv para limpar. Para usar um interpretador existente:
PYTHON=python3.12 ./run.sh.
Execute uma única PoC diretamente (dentro do venv):
.venv/bin/python poc/01_account_takeover.py
cve-2026-93355-lab/
├── mock_idp.py # ephemeral RS256 OIDC IdP (real signatures)
├── fake_litellm_db.py # in-memory stand-in for the Prisma user table
├── harness.py # configures & drives LiteLLM's real objects
├── poc/
│ ├── 01_account_takeover.py # unverified email -> victim's proxy_admin
│ ├── 02_persistent_rebind.py # sso_user_id overwrite -> persistent access
│ └── 03_patched_path.py # the email_verified guard that closes it
├── scripts/
│ └── verify_against_real_source.py # proves the PoCs run the real vulnerable code
├── assets/attack-flow.svg|.png # the diagram above
├── blog-cve-2026-93355.md # the guard0 write-up
└── run.sh
email_verified: true na fronteira de confiança antes que qualquer token
chegue à resolução de identidade; rejeite ou ignore e-mails não verificados.user_id_jwt_field),
e não a um e-mail mutável, e não permita que o fallback por e-mail o reatribua silenciosamente.proxy_admin indexada por e-mail é uma primitiva permanente de tomada de conta.