一个自包含、可丢弃的实验环境,用于复现
CVE-2026-93355:LiteLLM 的
JWT 认证通过回退到邮箱匹配来解析调用者身份——并且从不检查 email_verified 声明。攻击者只要能让自己的身份提供方账户签发一个携带受害者邮箱地址(未验证)的有效签名令牌,就能继承受害者的角色,最高可达
proxy_admin,而且该匹配会静默地将受害者存储的 SSO 绑定重写为攻击者的 subject,使接管持久化。
| 安全公告 | VulnCheck · OX Security |
| CVE | CVE-2026-93355 |
| 项目 | litellm(代理 / JWT 认证) |
| 受影响版本 | 根据公告为 ≤ 1.102.1——仍未修复:截至 2026-10-07,漏洞代码在 1.104.0(最新稳定版)和 main 中均未改变 |
| CVSS | 8.1 HIGH(AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N) |
| 弱点 | CWE-1390 — 弱认证 |
| 根本原因 | 基于邮箱的用户查找被信任,但未验证 email_verified |
| 发现者 | Nir Zadok 与 Moshe Siman Tov Bustan(OX Security) |
修复状态(2026-10-07): 目前尚无修复版本。公告仅列出
1.102.1,因为它是披露时的最新版本;LiteLLM 此后已发布1.103.x和1.104.0,而这两个版本中——以及main上——的_get_fuzzy_user_object仍然没有email_verified检查。实验环境固定使用1.102.1以保证可复现性;scripts/verify_against_real_source.py会在修复落地的那天发出提示。
本实验环境出于教育和防御目的复现一个已公开披露的漏洞。它完全在本地运行,不接触任何真实服务:
[email protected]
管理员记录并打印出来。没有真实账户、没有真实密钥,任何东西都不会离开你的机器。LiteLLM 的代理允许你使用来自所配置身份提供方的 JWT 进行认证。为了将该令牌映射到 LiteLLM 用户,它会调用 get_user_object(),该函数先尝试令牌的 subject(sso_user_id),未命中时回退到
_get_fuzzy_user_object()——即对 email 声明进行不区分大小写的匹配。在 LiteLLM 表中拥有该邮箱的人就是你所变成的身份。令牌的
email_verified 声明从未被读取——无论是在声明提取阶段
(JWTHandler.get_user_email)还是在解析阶段。因此,任何愿意为未验证邮箱签发令牌的 IdP(大多数都会,按设计如此——验证是依赖方的职责)都会让攻击者将自己的资料邮箱设为受害者的邮箱,出示该令牌,并继承受害者的角色。更糟的是,在邮箱匹配时
LiteLLM 会触发一个后台任务,用攻击者的 subject 覆盖受害者的 sso_user_id——因此之后每次登录都会直接解析到受害者的账户,无需邮箱,而且轮换邮箱也无济于事。

所有被测内容都是 LiteLLM 自身的代码,从固定版本的
litellm==1.102.1 包导入。实验环境仅提供它必须提供的两样东西:一个伪造的内存用户表和一个 mock IdP 密钥对。它不会重新实现漏洞逻辑。
_get_fuzzy_user_object 和 JWTHandler.get_user_email 执行 inspect.getsource(),并断言存在邮箱回退逻辑、存在 sso_user_id 重新绑定,且这两个函数均未引用 email_verified。→ scripts/verify_against_real_source.py_get_fuzzy_user_object 处理;最终解析到受害者的 proxy_admin 行。→ poc/01_account_takeover.pysso_user_id 重新绑定为攻击者的 subject;之后即使完全不带邮箱登录,也会直接解析为管理员,而且“修复重复邮箱”并不会撤销该绑定。→ poc/02_persistent_rebind.pyemail_verified,即可阻止该攻击,同时不影响已验证的 SSO 登录。→ poc/03_patched_path.py./run.sh
run.sh 会构建一个临时 .venv,安装 litellm[proxy]==1.102.1,运行基准真值检查,然后运行全部三个 PoC。首次运行会下载 LiteLLM,需要几分钟;删除 .venv 即可清理。要使用已有的解释器:
PYTHON=python3.12 ./run.sh。
直接运行单个 PoC(在 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,确保在任何令牌进入身份解析流程之前完成校验;拒绝或忽略未验证的邮箱。user_id_jwt_field),而非可变的邮箱,并且不要让邮箱回退逻辑在无提示的情况下重新绑定身份。proxy_admin 记录本身就是一种长期存在的接管原语。