
Riproduzione offline e benigna di CVE-2026-93355 — takeover dell'account tramite JWT con email non verificata in LiteLLM (esegue il codice reale di LiteLLM)
Un lab usa e getta e autonomo che riproduce
CVE-2026-93355: l'autenticazione
JWT di LiteLLM risolve l'identità del
chiamante ricadendo su una corrispondenza per email — e non verifica mai
il claim email_verified. Un attaccante che riesce a far emettere al proprio
account sull'identity provider un token firmato validamente contenente
l'indirizzo email della vittima (non verificato) eredita il ruolo della vittima,
fino a proxy_admin, e la corrispondenza riscrive silenziosamente il binding
SSO memorizzato della vittima con il subject dell'attaccante, rendendo il
takeover persistente.
| Advisory | VulnCheck · OX Security |
| CVE | CVE-2026-93355 |
| Progetto | litellm (proxy / autenticazione JWT) |
| Affette | ≤ 1.102.1 secondo l'advisory — ancora non corretto: il codice vulnerabile è invariato in 1.104.0 (ultima versione stabile) e in main al 2026-10-07 |
| CVSS | 8.1 HIGH (AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N) |
| Debolezza | CWE-1390 — autenticazione debole |
| Causa radice | Ricerca dell'utente basata sull'email considerata attendibile senza verificare email_verified |
| Scoperta | Nir Zadok & Moshe Siman Tov Bustan (OX Security) |
Stato della correzione (2026-10-07): non esiste ancora una release corretta. L'advisory elenca
1.102.1solo perché era la versione più recente al momento della divulgazione; LiteLLM ha da allora rilasciato1.103.xe1.104.0, e_get_fuzzy_user_objectin entrambe — e sumain— continua a non avere alcun controllo suemail_verified. Il lab fissa1.102.1per riproducibilità;scripts/verify_against_real_source.pysegnalerà il giorno in cui arriverà una correzione.
Questo riproduce una vulnerabilità divulgata pubblicamente per scopi educativi e difensivi. Viene eseguito interamente in locale e non tocca alcun servizio reale:
[email protected]
all'interno di questo processo e la stampa. Non c'è alcun account reale, nessuna chiave
reale, e nulla lascia la tua macchina.Il proxy di LiteLLM consente di autenticarsi con un JWT proveniente dall'identity
provider configurato. Per mappare quel token su un utente LiteLLM chiama get_user_object(),
che prova il subject del token (sso_user_id) e, in caso di mancata corrispondenza, ricade su
_get_fuzzy_user_object() — una corrispondenza case-insensitive sul claim email.
Chi possiede quell'email nella tabella di LiteLLM è chi diventi. Il claim
email_verified del token non viene mai letto — né durante l'estrazione dei claim
(JWTHandler.get_user_email) né durante la risoluzione. Quindi qualsiasi IdP che emetta
un token per un'email non verificata (la maggior parte lo farà, per design — la verifica è
compito della relying party) consente a un attaccante di impostare l'email del proprio profilo
su quella della vittima, presentare il token ed ereditare il ruolo della vittima. Peggio ancora,
in caso di corrispondenza per email LiteLLM avvia un task in background che sovrascrive
sso_user_id della vittima con il subject dell'attaccante — così ogni login successivo
si risolve direttamente sull'account della vittima senza bisogno dell'email, e cambiare
l'email non aiuta.

Tutto ciò che è sotto test è codice di LiteLLM stesso, importato dal pacchetto
litellm==1.102.1 fissato. Il lab fornisce solo due cose necessarie: una finta
tabella utenti in memoria e una coppia di chiavi mock per l'IdP. Non reimplementa
la logica vulnerabile.
inspect.getsource() sulla funzione installata
_get_fuzzy_user_object e su JWTHandler.get_user_email e verifichiamo che il fallback sull'email
esista, che il rebind di sso_user_id esista e che nessuna delle due funzioni
faccia riferimento a email_verified. → scripts/verify_against_real_source.py_get_fuzzy_user_object; viene
risolto sulla riga proxy_admin della vittima. → poc/01_account_takeover.pysso_user_id della vittima al subject dell'attaccante; un login successivo senza alcuna email
viene risolto direttamente su admin, e "correggere l'email duplicata" non lo revoca.
→ poc/02_persistent_rebind.pyemail_verified prima del fallback sull'email blocca
l'attacco lasciando funzionanti i login SSO verificati. → poc/03_patched_path.py./run.sh
run.sh crea un .venv temporaneo, installa litellm[proxy]==1.102.1, esegue il
controllo ground-truth, poi tutti e tre i PoC. La prima esecuzione scarica LiteLLM e richiede
qualche minuto; elimina .venv per fare pulizia. Per usare un interprete esistente:
PYTHON=python3.12 ./run.sh.
Esegui un singolo PoC direttamente (all'interno del 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 al confine di fiducia prima che qualsiasi token
raggiunga la risoluzione dell'identità; rifiuta o ignora le email non verificate.user_id_jwt_field),
non a un'email modificabile, e non lasciare che il fallback sull'email la riassoci silenziosamente.proxy_admin associata a un'email è una primitiva di takeover permanente.