Open WebUI CVE-2026-54017
HIGHSeverity by source
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N
Network-reachable proxy, low complexity, authenticated low-privilege grant required (PR:L), scope change to upstream terminal server, high confidentiality and low integrity since traversal can also reach write-capable upstream endpoints.
Primary rating from Vendor (https://github.com/open-webui/open-webui).
CVSS VectorVendor: https://github.com/open-webui/open-webui
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N
Lifecycle Timeline
2DescriptionCVE.org
Summary
The terminal-server reverse proxy in backend/open_webui/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:
- Raw path forwarding / single-encoded traversal (original report).
- A bypass of the subsequently-added
_sanitize_proxy_pathmitigation 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:
# routers/terminals.py
@router.api_route('/{server_id}/{path:path}', methods=PROXY_METHODS)
async def proxy_terminal(server_id, path, request, user=Depends(get_verified_user)):
...
safe_path = _sanitize_proxy_path(path)
if safe_path is None:
return JSONResponse({'error': 'Invalid path'}, status_code=400)
target_url = f'{base_url}/{safe_path}'
policy_id = connection.get('policy_id')
if policy_id:
target_url = f'{base_url}/p/{policy_id}/{safe_path}'Access requires has_connection_access(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 (target_url = f"{base_url}/{path}"), so single-encoded traversal escaped the intended scope:
GET /api/v1/terminals/server1/..%2F..%2F..%2Finternal-api/secrets
# proxied to: {base_url}/../../../internal-api/secretsThis vector is closed at HEAD: _sanitize_proxy_path 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 _sanitize_proxy_path
_sanitize_proxy_path decodes the path only once before the .. check, so a double-encoded payload survives:
def _sanitize_proxy_path(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 {base_url}/%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 _sanitize_proxy_path as %2e%2e/%2e%2e/sensitive-file
# upstream decodes -> ../../sensitive-fileThe policy_id form ({base_url}/p/{policy_id}/{safe_path}) 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:
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 NoneThis 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_sanitize_proxy_pathmitigation.
AnalysisAI
Authenticated path traversal and SSRF in Open WebUI's terminal-server reverse proxy (versions <= 0.9.5) allows non-admin users with granted terminal access to escape the intended path or policy scope using double-encoded traversal sequences (%252e%252e). The flaw bypasses the project's own _sanitize_proxy_path mitigation, which performs only a single URL-decode pass before the '..' check, letting attackers reach unintended endpoints, files, and internal services routed by the terminal server. …
Unlock full vulnerability intelligence
- Risk assessment & exploitation conditions
- Attack chain visualization
- Remediation with exact patch versions
- Threat intelligence from 22 sources
- Personal watchlist & email alerts
Free forever · No credit card required
Attack ChainAIDerived
Hypothetical attack flow derived from CVE metadata
Vulnerability AssessmentAI
| Exploitation | Attacker must hold a valid Open WebUI user account (get_verified_user) AND be explicitly granted access by an administrator to at least one configured terminal-server connection (has_connection_access must pass); no admin role is required. … Additional conditions and limiting factors are described in the full assessment. |
| Risk Assessment | CVSS 3.1 score of 7.7 (AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N) reflects network-reachable, low-complexity exploitation by an authenticated low-privilege user, with a scope change because the impacted component (the upstream terminal server and internal services) is distinct from the vulnerable Open WebUI instance. … Full risk analysis with EPSS, KEV, and SSVC signal comparison available after sign-in. |
| Exploit Scenario | An authenticated Open WebUI user who has been granted access to any configured terminal server issues a request such as GET /api/v1/terminals/server1/%252e%252e/%252e%252e/sensitive-file; _sanitize_proxy_path single-decodes this to %2e%2e/%2e%2e/sensitive-file, which passes the '..' prefix check, and the upstream terminal server then decodes it again and resolves ../../sensitive-file. Where a policy_id is configured (base_url/p/{policy_id}/{safe_path}), the same trick lets the attacker escape their assigned policy namespace and read other policies or files at the terminal-server root, or pivot SSRF-style into internal services the terminal server can reach. … |
| Remediation | Vendor-released patch: upgrade Open WebUI to 0.9.6 or later, where _sanitize_proxy_path now loops unquote up to eight times until the string stabilizes before normpath and the '..' check, neutralizing %2e%2e, %252e%252e, %25252e%25252e, and mixed forms like ..%2f..%2f. … Detailed patch versions, workarounds, and compensating controls in full report. |
Recommended ActionAI
Within 24 hours: enumerate all systems running Open WebUI versions 0.9.5 or earlier with terminal-server functionality; audit which users hold terminal access privileges. …
Sign in for detailed remediation steps and compensating controls.
Threat intelligence, references, and detailed analysis are available after sign-in.
Wazuh SIEM platform versions 4.4.0 through 4.9.0 contain an unsafe deserialization vulnerability in the DistributedAPI t
BentoML version 1.4.2 and earlier contains an unauthenticated remote code execution vulnerability through insecure deser
pgAdmin 4 contains critical remote code execution vulnerabilities in the Query Tool download and Cloud Deployment endpoi
The renderLocalView function in render/views.py in graphite-web in Graphite 0.9.5 through 0.9.10 uses the pickle Python
BentoML is a Python library for building online serving systems optimized for AI apps and model inference. Rated critica
OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h does not properly restrict processing of ChangeCiph
pyLoad download manager version prior to 0.5.0b3.dev77 exposes the Flask SECRET_KEY through an unauthenticated endpoint.
In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse
Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/
pyLoad is the free and open-source Download Manager written in pure Python. Rated medium severity (CVSS 5.3), this vulne
Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301
Cross-user flow execution in Langflow (< 1.9.1) lets any authenticated API-key holder run another user's flow by passing
Same weakness CWE-22 – Path Traversal
View allSame technique Path Traversal
View allShare
External POC / Exploit Code
Leaving vuln.today
GHSA-r2wg-2mcr-66rv