
Reproducción inofensiva y offline de CVE-2026-93355 — toma de control de cuenta mediante JWT con correo electrónico no verificado en LiteLLM (ejecuta el código real de LiteLLM)
Un laboratorio autocontenido y desechable que reproduce
CVE-2026-93355: la autenticación JWT de LiteLLM
resuelve la identidad de quien llama recurriendo a una coincidencia de correo
electrónico — y nunca comprueba la afirmación email_verified. Un atacante que pueda
hacer que su propia cuenta de proveedor de identidad emita un token firmado válidamente que contenga la
dirección de correo de una víctima (sin verificar) hereda el rol de la víctima, hasta
proxy_admin, y la coincidencia reescribe silenciosamente el vínculo SSO almacenado de la víctima
al sujeto del atacante, haciendo que la toma de control sea persistente.
| Aviso | VulnCheck · OX Security |
| CVE | CVE-2026-93355 |
| Proyecto | litellm (proxy / autenticación JWT) |
| Afectados | ≤ 1.102.1 según el aviso — aún sin parchear: el código vulnerable no ha cambiado en 1.104.0 (última versión estable) ni en main a fecha de 2026-10-07 |
| CVSS | 8.1 ALTO (AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N) |
| Debilidad | CWE-1390 — autenticación débil |
| Causa raíz | Búsqueda de usuario basada en correo electrónico confiada sin verificar email_verified |
| Descubrimiento | Nir Zadok y Moshe Siman Tov Bustan (OX Security) |
Estado de la corrección (2026-10-07): todavía no existe una versión corregida. El aviso indica
1.102.1solo porque era la versión más reciente en el momento de la divulgación; LiteLLM ha publicado desde entonces1.103.xy1.104.0, y_get_fuzzy_user_objecten ambas — y enmain— sigue sin ninguna comprobación deemail_verified. El laboratorio fija1.102.1por reproducibilidad;scripts/verify_against_real_source.pyavisará el día en que se publique una corrección.
Esto reproduce una vulnerabilidad divulgada públicamente con fines educativos y defensivos. Se ejecuta íntegramente en local y no toca ningún servicio real:
[email protected]
dentro de este proceso y la imprime. No hay ninguna cuenta real, ninguna clave real
y nada sale de tu máquina.El proxy de LiteLLM permite autenticarse con un JWT de tu proveedor de identidad
configurado. Para asignar ese token a un usuario de LiteLLM llama a get_user_object(),
que prueba el sujeto del token (sso_user_id) y, si no hay coincidencia, recurre a
_get_fuzzy_user_object() — una coincidencia sin distinción de mayúsculas sobre la afirmación email.
Quien sea dueño de ese correo en la tabla de LiteLLM es en quien te conviertes. La afirmación
email_verified del token nunca se lee — ni durante la extracción de afirmaciones
(JWTHandler.get_user_email) ni durante la resolución. Así, cualquier IdP que emita un token para un correo
no verificado (la mayoría lo hará, por diseño — la verificación es
responsabilidad de la parte que confía) permite a un atacante poner el correo de su perfil al de una víctima,
presentar el token y heredar el rol de la víctima. Peor aún, ante una coincidencia de correo
LiteLLM lanza una tarea en segundo plano que sobrescribe el sso_user_id de la víctima con
el sujeto del atacante — de modo que cada inicio de sesión posterior resuelve directamente a la
cuenta de la víctima sin necesidad de correo, y rotar el correo no ayuda.

Todo lo que se somete a prueba es código propio de LiteLLM, importado del paquete
litellm==1.102.1 fijado. El laboratorio solo aporta dos cosas imprescindibles: una tabla de usuarios
falsa en memoria y un par de claves de IdP simulado. No reimplementa la
lógica vulnerable.
inspect.getsource() sobre el _get_fuzzy_user_object y JWTHandler.get_user_email instalados y verificamos que existe el respaldo por correo electrónico, que existe la reasignación de sso_user_id y que ninguna de las dos funciones hace referencia a email_verified. → scripts/verify_against_real_source.py_get_fuzzy_user_object real; se resuelve en la fila proxy_admin de la víctima. → poc/01_account_takeover.pysso_user_id de la víctima al subject del atacante; un inicio de sesión posterior sin ningún correo electrónico se resuelve directamente en admin, y "corregir el correo electrónico duplicado" no lo revoca. → poc/02_persistent_rebind.pyemail_verified antes del respaldo por correo electrónico bloquea el ataque sin dejar de permitir los inicios de sesión SSO verificados. → poc/03_patched_path.py./run.sh
run.sh crea un .venv desechable, instala litellm[proxy]==1.102.1, ejecuta la comprobación de referencia y, a continuación, las tres PoC. La primera ejecución descarga LiteLLM y tarda unos minutos; elimine .venv para limpiar. Para usar un intérprete existente: PYTHON=python3.12 ./run.sh.
Ejecute una sola PoC directamente (dentro 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 en el límite de confianza antes de que cualquier token llegue a la resolución de identidad; rechazar o ignorar los correos electrónicos no verificados.user_id_jwt_field), no a un correo electrónico mutable, y no permitir que el respaldo por correo electrónico la revincule silenciosamente.proxy_admin indexada por correo electrónico es una primitiva de toma de control permanente.