# Open WebUI: Terminal proxy forwards a spoofable, integrity-unbound user identity to the upstream (X-User-Id header and ws_terminal session_id query injection)

Open WebUI's terminal proxy forwards an unsigned user identity; unencoded session_id query injection lets any user spoof another's user_id and attach to their live PTY.

- Payout: Research
- Severity: high
- Program: Open WebUI
- Found by: smoke-wolf, rexpository
- Disclosed: 2026-07-24
- CVE: CVE-2026-59224
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-j657-m4c4-24jq
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-j657-m4c4-24jq
- Tags: area:web domain:web impact:ato impact:internal-access kind:research platform:ghsa sev:high tech:python tech:websocket technique:source-review vuln:access-control vuln:auth-bypass

**Target:** Terminal proxy WebSocket/HTTP endpoints (backend/open_webui/routers/terminals.py)
**Root cause:** The proxy forwards user identity as an unsigned X-User-Id header / user_id query param with no integrity binding, and ws_terminal interpolates the unvalidated, unencoded session_id path parameter into the upstream URL before appending user_id.
**Steps:**
1. Authenticate as a normal user and reach the ws_terminal WebSocket endpoint.
2. Supply a session_id containing an encoded `?`/`&` so that after Open WebUI's single decode the upstream re-decodes it, e.g. injecting `user_id=<victim>` ahead of the appended `?user_id=<caller>`.
3. Upstream URL becomes `{ws_base}/p/{policy_id}/api/terminals/{session_id}?user_id=<victim>&user_id=<caller>`; query parsing binds the first occurrence.
4. Backend coordinator resolves the spoofed user's terminal scope; with a known active session ID (e.g. chat-scoped ID from a shared chat) attach to the victim's live PTY.
**How found:** Noticed ws_terminal builds the upstream URL by concatenating session_id without the _sanitize_proxy_path used by the HTTP sibling, and that the appended user_id query param could be preceded by an injected one surviving single-decode/re-decode.
**Exploitation:** A normal authenticated user injects an attacker-chosen user_id via the unencoded session_id into the upstream WebSocket URL; the backend coordinator binds the first user_id occurrence and scopes the terminal container to the victim, allowing attachment to their live PTY when a session ID is known. HTTP path variant spoofs X-User-Id at any upstream trusting the header.
**Escalation:** Combined with a known active session ID surfaced through a shared chat to reach another user's live PTY.
**Insight:** When a proxy appends an authorization query param after an unencoded path segment, smuggle an encoded `?`/`&` through the path so the upstream's second decode binds your injected param first; compare sibling handlers for missing sanitizers.

## Proof of concept (copied exactly from the original report)

**ws_terminal URL built from unvalidated session_id**

```
upstream_url = f'{ws_base}/p/{policy_id}/api/terminals/{session_id}'
   upstream_url += f'?{urllib.parse.urlencode({"user_id": user.id})}'
```

**proxy_terminal sets unsigned X-User-Id header**

```
headers['X-User-Id'] = user.id
```

