CVE-2026-59219
GHSA-855v-hq7w-jmjwRealtime endpoints accept Redis-revoked JWTs after sign-out
Open WebUI — realtime authentication[pip · open-webui]
A logout that didn't fully log out — revoked session tokens kept authenticating Open WebUI's realtime connections long after the HTTP layer had started rejecting them.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N> Overview
Open WebUI is a widely self-hosted interface for running large language models. When it is deployed with Redis, it supports true JWT revocation: signing out or an OIDC back-channel logout records the revocation in Redis so a stolen or leaked token can be invalidated before it expires on its own.
That guarantee held for the normal HTTP API but not for the realtime surfaces. Socket.IO connections and the terminal websocket validated tokens with a signature-and-expiry check only, never consulting the Redis revocation state. A token that had been explicitly revoked could therefore keep opening new realtime sessions — defeating the one control meant to contain a compromised session.
> How revocation is supposed to work
Two events write a revocation record into Redis: a per-token entry keyed by the token's `jti` on sign-out, and a per-user `revoked_at` timestamp on OIDC back-channel logout. The HTTP authentication dependency reads those keys on every request and rejects a revoked token with a 401.
# HTTP requests consult Redis and reject a revoked token
if data.get("jti") and not await is_valid_token(request, data):
raise HTTPException(status_code=401, detail="Invalid token")> Root cause
The realtime authentication paths decoded the token but stopped there. `decode_token()` verifies the signature and expiry; it does not look at the Redis revocation keys. So the same token that HTTP rejected with a 401 was still accepted by the realtime handlers.
# realtime paths — signature + expiry only, revocation never checked
data = decode_token(auth["token"])
# never reads {prefix}:auth:token:{jti}:revoked
# or {prefix}:auth:user:{id}:revoked_at- Socket.IO connect, user-join, join-channels and join-note in backend/open_webui/socket/main.py.
- Terminal websocket first-message authentication in backend/open_webui/routers/terminals.py.
- The revocation check in backend/open_webui/utils/auth.py had only ever been wired into the HTTP dependency.
> Impact
A JWT revoked by sign-out or back-channel logout continued to authenticate new realtime connections — exactly the tokens an operator expects to be dead after a logout or an identity-provider session kill.
With the revoked token an attacker could register in the session pool as the victim, join their user, channel and note rooms, and receive realtime channel messages, collaborative-note updates and presence. Where terminal servers were configured, the same token passed terminal-websocket authentication.
Blast radius was bounded to realtime surfaces: the HTTP layer still returned 401 for the revoked token, so REST reads and state-changing REST endpoints stayed out of reach.
> Proof of concept
On a Redis-backed deployment (validated on v0.9.6 and main), the reporter demonstrated the split: after calling the sign-out endpoint, the same token returns 401 over HTTP while a Socket.IO user-join with that token still authenticates, and the terminal websocket reaches terminal-server lookup instead of rejecting the token as invalid.
> The fix
Version 0.10.0 factors the revocation logic into a single shared helper and applies it everywhere a token is accepted. Both the per-token `jti` revocation and the per-user `revoked_at` timestamp are covered, using the main-app Redis where revocations are stored.
# one helper, checked on every entry point now
is_token_revoked(redis, decoded) # per-token jti + per-user revoked_at
# Socket.IO handlers and the terminal WS reject tokens that fail it;
# HTTP is_valid_token delegates to the same helper, so HTTP is unchanged.- Patched in a focused, three-file change: socket/main.py, routers/terminals.py and utils/auth.py.
- Realtime handlers now fail closed on a revoked token, matching HTTP behavior.
> Classification
Scored High (CVSS 7.1) and classified as CWE-613, Insufficient Session Expiration. The published CVE record also carries CISA ADP / SSVC enrichment: Exploitation `poc`, Automatable `no`, Technical Impact `partial`.
The SSVC "poc" value is a record-enrichment classification reflecting that a proof of concept exists — not a claim that any real-world system was exploited.
> Disclosure timeline
- 2026-06-04Advisory opened; credited as reporter.
- 2026-06-06Fix linked to pull request #25764; Classic298 credited as coordinator.
- 2026-07-02Open WebUI published the repository advisory and requested a CVE.
- 2026-07-07GitHub assigned CVE-2026-59219.
- 2026-07-09GitHub CNA published the CVE record.