One Unverified Email Claim Becomes a Permanent Admin — LiteLLM CVE-2026-93355 Is Still Unpatched
OX Security published CVE-2026-93355 on 29 September 2026, and the interesting part is not the bug class. It is that the bug does not just let an attacker impersonate an existing LiteLLM admin — it rewrites the victim's account so the attacker's own identity becomes the account, permanently, as a side effect of the first request. The advisory was filed with the vendor on 18 May 2026. As of publication there is no patch.
NVD recorded the CVE on 28 September 2026 as CVSS v3.1 8.1 (High), CWE-290, alongside GHSA-g83g-3c4g-cfm5. This is the second LiteLLM authentication boundary to make headlines this month, after CVE-2026-59822 became the first MCP flaw in CISA's KEV catalog.
The two-stage lookup that trusts the wrong field
LiteLLM resolves a JWT-bearing request to a user in two stages. Stage one is a direct match on user_id or sso_user_id. On a miss — which is the normal outcome for any identity's very first request — it calls the fallback, _get_fuzzy_user_object, which matches on the token's email claim instead.
We read the current main branch of BerriAI/litellm to confirm the behaviour independently. Two things in litellm/proxy/auth/auth_checks.py matter:
- The email match is plain equality, case-insensitive, and unverified. The fallback issues a
find_firstonuser_emailwithmode: "insensitive". Nothing in that function checks whether the presented email was ever verified. - A hit does not just authenticate — it rebinds. On a successful email match with an
sso_user_idpresent, the code fires anasyncio.create_taskthat updates the matched row'ssso_user_idto the attacker's token subject. It is not awaited, so it lands regardless of what the rest of the request does. The source comment is candid: "background task to update user with sso id."
The claim that reaches this path is never validated upstream either. email_verified does not appear anywhere in litellm/proxy/auth/handle_jwt.py — we grepped the current file and confirmed zero occurrences. get_user_email reads whatever sits at the configured claim path and returns it as-is.
Why the rebind is the actual severity driver
A one-shot impersonation on an unverified claim would already be a serious finding. The write turns it into persistence, and it inverts the usual remediation logic:
- The forged claim is needed exactly once. After the rebind, the attacker's subject matches on the fast path. No fallback, no email, no forged claim — just a normal, validly-signed token from a trusted issuer, resolving directly to a
proxy_adminrow. - Fixing the claim upstream afterwards does not undo it. Enforcing
email_verifiedat your IdP tomorrow closes the door; it does not evict anyone already inside, because their access no longer depends on the email path. - The victim's own login is now the anomaly. The legitimate owner's subject no longer matches the row it used to. Depending on configuration, the real admin gets provisioned a fresh, lower-privileged account — which looks like a support ticket, not an incident.
That last property is why this belongs in a hunt, not just a patch queue. The database state is the evidence.
What the attacker actually needs
Less than the CVSS vector's PR:L suggests in practice. The prerequisites are: a validly-signed JWT from an issuer the proxy already trusts, carrying the victim's email address in the configured claim, and a victim account that exists in LiteLLM's user table.
The uncomfortable detail is how ordinary the first condition is. Plenty of IdP and self-service signup flows populate an email claim before — or entirely without — confirming mailbox ownership, and issue email_verified: false alongside it. LiteLLM never reads that field, so the two tokens are indistinguishable to the proxy. No misconfiguration is required on the defender's side; the default posture is the vulnerable one.
The one adjacent control that exists, the user_allowed_email_domain allowlist, is off by default — is_enforced_email_domain() returns false unless an administrator sets it. And even when set, it blocks foreign-domain spoofing only. A same-domain attacker claiming a colleague's address passes it cleanly.
A note on the two CVSS scores
OX's advisory states CVSS v3.1 8.8 with an availability impact (A:H). NVD and GitHub record 8.1 with A:N. Both vectors are otherwise identical, and neither is wrong exactly — an attacker holding proxy_admin can plainly disrupt the proxy, so the disagreement is a scoping judgement about whether that counts as availability impact to the vulnerable component. Treat the delta as noise. Nothing about your response changes between 8.1 and 8.8; what changes your response is that there is no fix.
Why the target matters more than the score
LiteLLM is an AI gateway with roughly 59,000 GitHub stars, deployed as the single chokepoint through which an organisation's model traffic flows. That position is the whole point of the product — and it means the proxy holds, in one place, the upstream provider API keys for OpenAI, Anthropic, Azure, Bedrock, Vertex and everything else configured, plus every virtual key, budget, and spend record.
An account takeover here is not lateral movement into one more SaaS tenant. A proxy_admin session reaches key/list and user/list, which is to say it reaches the organisation's provisioned credentials for every model provider behind the gateway. This is the same concentration-of-secrets problem that made the MCP auth bypass a KEV entry rather than a routine advisory.
The disclosure timeline is the other story
OX reports: vulnerability reported 18 May 2026; follow-up requesting status 27 July 2026, no response; public disclosure proceeding 14 September 2026 after roughly 120 days of silence. The CVE was assigned 17 September, published by NVD 28 September, and the research write-up landed 29 September.
OX's advisory cites the then-current release, 1.100.1 (published 10 September 2026), as affected and unpatched. We checked further: PyPI's current LiteLLM release is 1.103.0, and the fallback-plus-rebind code path is present unchanged in the repository's main branch as of this writing. There is no upstream fix to apply. Treat every deployment configured for JWT authentication as affected until BerriAI ships one.
What to do today
- Enforce
email_verifiedbefore the token reaches the proxy. This is the one control that directly neutralises the attack, and it has to live outside LiteLLM because LiteLLM does not read the field. Enforce it at the IdP, or strip and rewrite theemailclaim at a reverse proxy or claim-mapping layer for any token lackingemail_verified: true. - Pin identity to a stable subject claim, not email. Configure
user_id_jwt_fieldso every trusted issuer supplies a unique, stable subject from the first login, minimising how often the fallback is reached at all. - Hunt the rebind, do not just block it. The exploit leaves a specific artefact: an
sso_user_idon a privileged row that does not correspond to the subject that account's real owner authenticates with. Query the user table for privileged accounts whosesso_user_idchanged, and reconcile each against your IdP's subject for that person. This is a database question, not a log question — and the reports date the disclosure window back to May. - Do not leave privileged accounts pre-provisioned but unlinked. The attack needs an existing row whose email is guessable and whose identity has not yet been bound. Pre-created
proxy_adminaccounts waiting for their first SSO login are the ideal target; create them at first login instead, or bind them immediately. - Set
user_allowed_email_domainanyway. It is defence-in-depth, not a fix — it stops external-domain spoofing and nothing else — but it is a one-line configuration that is off by default. - Rotate upstream provider keys if you find a rebind. Admin context on an AI gateway means the provider credentials behind it should be considered exposed, on the same reasoning we applied to LiteLLM's earlier KEV entry.
The pattern worth naming
Identity fallbacks exist because first logins are genuinely awkward: the proxy has a token for someone it has never seen, and a user row it would like to match. Every AI gateway, agent platform, and MCP host solves that with some variation of "try the strong identifier, then try something friendlier." The friendlier identifier is almost always email, and email is almost always the one claim the issuer will hand out before verifying anything.
This is the same structural failure as the WSO2 JWT bypass that reached KEV five months after its patch: a token that is validly signed is treated as a token whose contents are trustworthy. Signature validity says the issuer produced the token. It says nothing about whether the issuer checked the claims inside it. If your authentication code resolves identity on any claim other than the subject, the question to ask is not "is this token valid" but "who, exactly, verified this field, and when."
Sources:
- OX Security — “CVE-2026-93355: Account Takeover in LiteLLM” by Nir Zadok & Moshe Siman Tov Bustan (29 September 2026; technical analysis, proof of concept, disclosure timeline, mitigations)
- NVD — CVE-2026-93355 (published 28 September 2026; CVSS v3.1 8.1 High, CVSS v4.0 7.6)
- GitHub Advisory Database — GHSA-g83g-3c4g-cfm5, “LiteLLM contains a weak authentication vulnerability” (28 September 2026)
- VulnCheck — “LiteLLM Weak JWT Authentication via Email-Based User Lookup”
- BerriAI/litellm —
litellm/proxy/auth/auth_checks.pyandlitellm/proxy/auth/handle_jwt.py,mainbranch (code paths independently verified for this briefing, 29 September 2026)