CVE-2026-54017: Open WebUI: Path traversal / SSRF in terminal server proxy via encoded path traversal
Summary
The terminal-server reverse proxy in backend/openwebui/routers/terminals.py does not fully confine the user-controlled path segment before forwarding it to an admin-configured terminal server. An authenticated user who has been granted access to a terminal server can craft path values containing encoded ../ traversal sequences that escape the intended path (or policy) scope on that server, reaching unintended endpoints and files on the terminal-server host. Where the terminal server fans requests out to internal services, this also gives SSRF-style reach into those services.
This is a separate code path from the /api/v1/retrieval/process/web SSRF (GHSA-c6xv-rcvw-v685), with its own input. Two distinct vectors are consolidated here:
1. Raw path forwarding / single-encoded traversal (original report). 2. A bypass of the subsequently-added sanitizeproxypath mitigation using double-encoded dots (%252e%252e).
The attacker-controlled input is the request path, supplied by the non-admin user, not anything an administrator configures, so this is not an admin-trust / Rule-9 situation.
Affected code
The proxy route forwards an arbitrary trailing path to the configured terminal server:
python routers/terminals.py @router.apiroute('/{serverid}/{path:path}', methods=PROXYMETHODS) async def proxyterminal(serverid, path, request, user=Depends(getverifieduser)): ... safepath = sanitizeproxypath(path) if safepath is None: return JSONResponse({'error': 'Invalid path'}, statuscode=400) targeturl = f'{baseurl}/{safepath}' policyid = connection.get('policyid') if policyid: targeturl = f'{baseurl}/p/{policyid}/{safepath}'
Access requires hasconnectionaccess(user, connection, ...), i.e. a non-admin user the administrator has granted to that terminal server.
Vector 1 — single-encoded traversal (original)
The path was originally concatenated to the base URL with no sanitization (targeturl = f"{baseurl}/{path}"), so single-encoded traversal escaped the intended scope:
GET /api/v1/terminals/server1/..%2F..%2F..%2Finternal-api/secrets proxied to: {baseurl}/../../../internal-api/secrets
This vector is closed at HEAD: sanitizeproxypath now URL-decodes once, runs posixpath.normpath, strips leading slashes, and rejects results beginning with .. (unquote('..%2F..%2F') -> '../../' -> normpath -> '../..' -> rejected).
Vector 2 — double-encoded bypass of sanitizeproxypath
sanitizeproxypath decodes the path only once before the .. check, so a double-encoded payload survives:
python def sanitizeproxypath(path: str) -> str | None: decoded = unquote(path) # single decode pass only normalized = posixpath.normpath(decoded) cleaned = normalized.lstrip('/') if cleaned.startswith('..') or cleaned == '.': return None ...
unquote('%252e%252e/secret') yields %2e%2e/secret (not ..), which normpath leaves unchanged and which does not start with .., so it passes the check. The proxy then forwards {baseurl}/%2e%2e/secret, and the upstream terminal server decodes %2e%2e into .. and resolves the traversal the check was meant to prevent.
GET /api/v1/terminals/server1/%252e%252e/%252e%252e/sensitive-file passes sanitizeproxypath as %2e%2e/%2e%2e/sensitive-file upstream decodes -> ../../sensitive-file
The policyid form ({baseurl}/p/{policyid}/{safepath}) is the higher-impact target: traversal escapes the policy namespace and reaches other policies or the terminal-server root.
Impact
An authenticated user with access to a terminal server can escape the intended path/policy scope on that server, reaching unintended endpoints and files, and, where the terminal server routes onward to internal services, reach those services. CWE-22 (Path Traversal) and CWE-918 (SSRF).
Fix
Decode the proxy path until it is stable before normalising and checking, so no depth of encoding can smuggle a traversal sequence past the check to be re-decoded upstream:
python decoded = path for in range(8): once = unquote(decoded) if once == decoded: break decoded = once normalized = posixpath.normpath(decoded) cleaned = normalized.lstrip('/') if cleaned.startswith('..') or cleaned == '.': return None
This rejects %2e%2e, %252e%252e, %25252e%25252e, ..%2f..%2f, etc., while leaving legitimate paths (including singly-encoded characters such as %20) intact.
Credits
- Tulgaaaaaaaa — original report (terminal-proxy path SSRF / single-encoded traversal). - sermikr0 — double-encoded (%252e%252e) bypass of the sanitizeproxypath mitigation.
Other sources
Open WebUI is a self-hosted artificial intelligence platform designed to operate entirely offline. Prior to 0.9.6, the terminal-server reverse proxy in backend/openwebui/routers/terminals.py does not fully confine the user-controlled path segment before forwarding it to an admin-configured terminal server. An authenticated user who has been granted access to a terminal server can craft path values containing encoded ../ traversal sequences that escape the intended path (or policy) scope on that server, reaching unintended endpoints and files on the terminal-server host. Where the terminal server fans requests out to internal services, this also gives SSRF-style reach into those services. This is a separate code path from the /api/v1/retrieval/process/web SSRF (GHSA-c6xv-rcvw-v685), with its own input. Two distinct vectors are consolidated here: first, raw path forwarding / single-encoded traversal (original report); and second, a bypass of the subsequently-added sanitizeproxypath mitigation using double-encoded dots (%252e%252e). The attacker-controlled input is the request path, supplied by the non-admin user, not anything an administrator configures, so this is not an admin-trust / Rule-9 situation. Version 0.9.6 fixes the issue.
— MITRE
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
pip/open-webuito a version that resolves this vulnerability.Fixed in 0.9.6 - Configuration
Modify _sanitize_proxy_path to repeatedly URL-decode the incoming path until it is stable (for example, loop unquote up to 8 times as shown in the code), then run posixpath.normpath on the decoded value, lstrip('/') the normalized result, and return HTTP 400 if the result starts with '..' or is '.'; use the resulting safe_path when constructing the target_url.
backend/open_webui/routers/terminals.py (_sanitize_proxy_path) decode_until_stable_before_normalization = repeat URL-decode (unquote) until the decoded string no longer changes (or up to 8 iterations), then apply posixpath.normpath, strip leading slashes, and reject results beginning with '..' or equal to '.'
Event History
Frequently Asked Questions
What is the severity of CVE-2026-54017?
CVE-2026-54017 has a high severity rating of 7.7.
How do I fix CVE-2026-54017?
To fix CVE-2026-54017, ensure that the user-controlled 'path' segment is properly validated and sanitized before being forwarded to the terminal server.
What software is affected by CVE-2026-54017?
CVE-2026-54017 affects the open-webui software package.
What vulnerabilities are associated with CVE-2026-54017?
CVE-2026-54017 is associated with Path Traversal and SSRF (Server-Side Request Forgery) vulnerabilities.
What are the potential consequences of exploiting CVE-2026-54017?
Exploiting CVE-2026-54017 could allow authenticated users to access unauthorized paths on the terminal server.