CVE Patch Review

CVE-2026-59219 Root-Cause Fix for Open WebUI Real-Time Session Revocation Bypass

CVE-2026-59219 · Updated 2026-07-24 Root-cause

Summary

Open WebUI previously accepted JWTs on Socket.IO and terminal WebSocket paths based on successful decoding and presence of a user id, but did not consistently enforce Redis-backed revocation state. The patch threads Redis access into real-time authentication paths and updates token validation to operate on decoded claims plus an explicit Redis handle, closing the gap between stateless JWT verification and stateful logout/blacklist enforcement.

Analysis

Vulnerability

CVE-2026-59219 affects Open WebUI real-time interfaces where Socket.IO and terminal WebSocket authentication accepted decoded JWTs without consistently enforcing Redis-backed revocation state. Per the commit referenced by the CVE, the vulnerable code paths imported decode_token alone and gated access on decoded token presence and an id claim, which meant a token with a valid signature could continue to authorize real-time activity after logout or blacklist events if revocation markers existed only in Redis.

This is a classic split between stateless token verification and stateful session invalidation. HTTP request paths already had revocation-aware validation logic in open_webui.utils.auth, but the real-time endpoints did not reliably call it. As a result, revoked sessions could persist on long-lived channels, including administrative terminal proxy access, despite server-side intent to invalidate them. The issue is documented in the upstream code change and CVE records at the fixing commit, NVD, and CVE.org.

Vulnerable pattern in real-time handlers:
from open_webui.utils.auth import decode_token
...
if data is None or 'id' not in data:
    # reject

# Missing Redis-backed revocation validation here allowed revoked JWTs
# to remain usable on WebSocket / Socket.IO paths.

Patch

The patch introduces revocation-aware validation into the affected real-time entry points and refactors the validation helper so it can be used outside a request object. In backend/open_webui/routers/terminals.py, the import changes from decode_token to decode_token, is_valid_token, and the terminal WebSocket gate now rejects tokens unless await is_valid_token(data, getattr(ws.app.state, 'redis', None)) succeeds. In backend/open_webui/socket/main.py, the same helper is added and Redis is resolved from the ASGI app scope or a fallback REDIS object before validating decoded token data on multiple Socket.IO paths.

The helper in backend/open_webui/utils/auth.py is also reshaped from a request-bound signature to a decoded-claims-plus-Redis signature. The vulnerable form was async def is_valid_token(request, decoded) -> bool; the patched form is async def is_valid_token(decoded, redis=None) -> bool. Internally, Redis lookups for token-level revocation and user-level revocation timestamps now operate on the explicit Redis handle, allowing the same logic to be reused by HTTP, WebSocket, and Socket.IO authentication flows.

from open_webui.utils.auth import decode_token, is_valid_token

# terminals.py
if data is None or 'id' not in data or not await is_valid_token(data, getattr(ws.app.state, 'redis', None)):
    # reject connection

# socket/main.py
scope = (environ or {}).get('asgi.scope') or {}
fastapi_app = scope.get('app')
redis = getattr(getattr(fastapi_app, 'state', None), 'redis', None) or REDIS
if token_data is None or 'id' not in token_data or not await is_valid_token(token_data, redis):
    # reject event / connection

# utils/auth.py
async def is_valid_token(decoded, redis=None) -> bool:
    if redis:
        revoked = await redis.get(f'{REDIS_KEY_PREFIX}:auth:token:{jti}:revoked')
        revoked_at = await redis.get(f'{REDIS_KEY_PREFIX}:auth:user:{user_id}:revoked_at')

This patch is directly grounded in the upstream commit at 33b91bd8ae8a100a5a306c91441a7d0b422c4cde.

Review

Pros

The patch addresses the actual authorization gap rather than only tightening parsing or connection setup. By requiring is_valid_token(...) in terminal and Socket.IO handlers, it aligns real-time authentication with the existing revocation model used elsewhere. The helper refactor is also structurally sound: moving from a request-coupled API to an explicit decoded, redis API makes the validation primitive reusable across transports and reduces the chance that future non-HTTP code paths skip revocation checks because they lack a request object.

Another positive aspect is coverage breadth inside socket/main.py. The diff shows repeated insertion of ASGI-scope Redis resolution and revocation checks at multiple token validation sites, which suggests the fix was applied to more than just the initial connection handshake. That matters because event-level handlers can otherwise become alternate authorization paths even if connect-time checks are correct.

Cons

The patch appears repetitive in socket/main.py, with near-identical blocks extracting environ, scope, fastapi_app, and redis. While functionally acceptable, duplication increases maintenance risk and makes future omissions more likely if new handlers are added. A small helper for Redis resolution or a centralized authenticated-session accessor would reduce drift.

There is also an operational caveat in the helper signature: is_valid_token(decoded, redis=None) only performs revocation lookups when a Redis handle is available. That behavior is visible in the patch snippet via if redis:. If deployments accidentally run without Redis, or if a real-time path fails to resolve it and falls back to None, revocation enforcement degrades silently to signature-only acceptance. That may be an intentional compatibility choice, but from a security perspective it preserves a fail-open mode for revocation semantics.

Verdict

Root-cause.

The patch fixes the core defect: real-time endpoints were not consulting the authoritative revocation state, and now they do. The helper refactor removes the architectural reason those paths diverged from HTTP request validation, which is why this is better classified as a root-cause fix than a narrow bandaid. The remaining concern is not bypass persistence in the patched paths, but the broader fail-open behavior when Redis is unavailable. That is a separate hardening question rather than evidence that this specific CVE fix is incomplete.

References: upstream fixing commit, NVD entry, CVE.org record.

Sources