From 1a94efcc218f9a3271690c62a5e0ff548460d9f1 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sun, 14 Jun 2026 12:28:08 +0200 Subject: [PATCH] Encode terminal ws session_id to block upstream user_id query injection ws_terminal() interpolated the path parameter session_id directly into the upstream terminal WebSocket URL and then appended ?user_id=, with no encoding or validation (the HTTP sibling proxy_terminal runs _sanitize_proxy_path; this path ran nothing). An encoded '?'/'&' smuggled through session_id survives Open WebUI's single decode and is re-decoded by the upstream, injecting an attacker-chosen user_id ahead of the appended one. Query parsing binds the first occurrence, so the orchestrator resolves the spoofed user's terminal scope, letting a normal authenticated user present another user's identity to the upstream (CWE-116/863). Encode session_id as an opaque path segment with urllib.parse.quote(session_id, safe=''). This neutralises '?'/'#'/'&' at any decode depth (the upstream's single decode reverses only the quote, leaving the original delimiters inert as path content), while legitimate UUID session ids pass through unchanged. The appended user_id is then the only query parameter the upstream binds. The separate concern that the forwarded identity is a bearer claim with no integrity binding spans Open WebUI and the upstream terminal server and is not addressed here. Co-authored-by: rexpository <30176934+rexpository@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/open_webui/routers/terminals.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/backend/open_webui/routers/terminals.py b/backend/open_webui/routers/terminals.py index 6a942cf9b2..f9acdaa6fb 100644 --- a/backend/open_webui/routers/terminals.py +++ b/backend/open_webui/routers/terminals.py @@ -282,10 +282,14 @@ async def ws_terminal( import urllib.parse + # Encode session_id as an opaque path segment so it cannot smuggle '?'/'#'/'&' (at any + # decode depth) and inject an attacker-chosen user_id ahead of the one appended below. + safe_session_id = urllib.parse.quote(session_id, safe='') + if policy_id: - upstream_url = f'{ws_base}/p/{policy_id}/api/terminals/{session_id}' + upstream_url = f'{ws_base}/p/{policy_id}/api/terminals/{safe_session_id}' else: - upstream_url = f'{ws_base}/api/terminals/{session_id}' + upstream_url = f'{ws_base}/api/terminals/{safe_session_id}' if upstream_params: upstream_url += f'?{urllib.parse.urlencode(upstream_params)}'