
Reproduction bénigne et hors ligne de CVE-2026-93355 — prise de contrôle de compte par JWT avec e-mail non vérifié dans LiteLLM (exécute le code réel de LiteLLM)
Un laboratoire jetable et autonome qui reproduit
CVE-2026-93355 : l'authentification JWT de LiteLLM
résout l'identité d'un appelant en se rabattant sur une correspondance
d'email — et ne vérifie jamais la revendication email_verified. Un attaquant capable de
faire émettre par son propre compte de fournisseur d'identité un jeton signé valablement portant
l'adresse email d'une victime (non vérifiée) hérite du rôle de la victime, jusqu'à
proxy_admin, et la correspondance réécrit silencieusement la liaison SSO
stockée de la victime avec le sujet de l'attaquant, rendant la prise de contrôle persistante.
| Avis | VulnCheck · OX Security |
| CVE | CVE-2026-93355 |
| Projet | litellm (proxy / authentification JWT) |
| Affecté | ≤ 1.102.1 selon l'avis — toujours non corrigé : le code vulnérable est inchangé dans 1.104.0 (dernière version stable) et main au 2026-10-07 |
| CVSS | 8.1 ÉLEVÉ (AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N) |
| Faiblesse | CWE-1390 — authentification faible |
| Cause racine | Recherche d'utilisateur par email approuvée sans vérifier email_verified |
| Découverte | Nir Zadok & Moshe Siman Tov Bustan (OX Security) |
Statut du correctif (2026-10-07) : aucune version corrigée n'existe encore. L'avis ne liste
1.102.1que parce que c'était la version la plus récente au moment de la divulgation ; LiteLLM a depuis publié1.103.xet1.104.0, et_get_fuzzy_user_objectdans les deux — et surmain— n'a toujours pas de vérificationemail_verified. Le laboratoire épingle1.102.1pour la reproductibilité ;scripts/verify_against_real_source.pysignalera le jour où un correctif sera publié.
Ceci reproduit une vulnérabilité divulguée publiquement à des fins éducatives et défensives. Il s'exécute entièrement en local et ne touche aucun service réel :
[email protected]
préchargée dans ce processus et l'affiche. Il n'y a pas de vrai compte, pas de vraie
clé, et rien ne quitte votre machine.Le proxy de LiteLLM vous permet de vous authentifier avec un JWT provenant de votre fournisseur d'identité
configuré. Pour associer ce jeton à un utilisateur LiteLLM, il appelle get_user_object(),
qui essaie le sujet du jeton (sso_user_id) et, en cas d'échec, se rabat sur
_get_fuzzy_user_object() — une correspondance insensible à la casse sur la revendication email.
Celui qui possède cet email dans la table de LiteLLM est celui que vous devenez. La revendication
email_verified du jeton n'est jamais lue — ni lors de l'extraction des revendications
(JWTHandler.get_user_email) ni lors de la résolution. Ainsi, tout IdP qui émettra un jeton pour un email
non vérifié (la plupart le feront, par conception — la vérification est
la responsabilité de la partie de confiance) permet à un attaquant de définir l'email de son profil sur celui d'une victime,
de présenter le jeton et d'hériter du rôle de la victime. Pire encore, lors d'une correspondance par email,
LiteLLM déclenche une tâche en arrière-plan qui écrase le sso_user_id de la victime avec
le sujet de l'attaquant — ainsi chaque connexion ultérieure se résout directement sur le
compte de la victime sans avoir besoin d'email, et changer l'email n'aide pas.

Tout ce qui est testé est le propre code de LiteLLM, importé depuis le paquet
litellm==1.102.1 épinglé. Le laboratoire ne fournit que deux choses indispensables : une fausse
table d'utilisateurs en mémoire et une paire de clés IdP mock. Il ne réimplémente pas la
logique vulnérable.
inspect.getsource() sur la fonction _get_fuzzy_user_object et JWTHandler.get_user_email installées et vérifions que le repli sur l'email existe, que la reliaison de sso_user_id existe, et qu'aucune des deux fonctions ne référence email_verified. → scripts/verify_against_real_source.py_get_fuzzy_user_object ; il se résout vers la ligne proxy_admin de la victime. → poc/01_account_takeover.pysso_user_id de la victime au sujet de l'attaquant ; une connexion ultérieure sans aucun email se résout directement vers l'admin, et « corriger l'email en double » ne la révoque pas. → poc/02_persistent_rebind.pyemail_verified avant le repli sur l'email bloque l'attaque tout en laissant fonctionner les connexions SSO vérifiées. → poc/03_patched_path.py./run.sh
run.sh construit un .venv jetable, installe litellm[proxy]==1.102.1, exécute la vérification de référence, puis les trois PoC. Le premier lancement télécharge LiteLLM et prend quelques minutes ; supprimez .venv pour nettoyer. Pour utiliser un interpréteur existant :
PYTHON=python3.12 ./run.sh.
Exécuter un seul PoC directement (à l'intérieur du 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 à la frontière de confiance avant que tout jeton
n'atteigne la résolution d'identité ; rejeter ou ignorer les e-mails non vérifiés.user_id_jwt_field),
et non à un e-mail modifiable, et ne pas laisser le repli sur l'e-mail le relier silencieusement.proxy_admin indexée par e-mail constitue une primitive de prise de contrôle permanente.