From 841c9045d789005145274955e7ef60b1b11a9be9 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sun, 10 May 2026 18:08:12 +0200 Subject: [PATCH] fix: gate tool content updates behind workspace.tools to match create endpoint (#24513) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: gate tool content updates behind workspace.tools to match create endpoint `update_tools_by_id` (routers/tools.py:452) authorizes a caller as long as they are the tool's owner, hold a `write` access grant on the tool, or are an admin. This means a verified user who has been given a write grant on a tool — typically as part of a metadata-collaboration workflow (edit description, adjust valves, manage access grants) — can also overwrite the tool's Python source. Because `load_tool_module_by_id` further down calls `exec(content, module.__dict__)` at module-import time, anything the new content puts outside the `class Tools:` body executes immediately on the server with the worker's privileges (root in the default Docker deployment). The `create_new_tools` endpoint already requires `workspace.tools` (or `workspace.tools_import`) precisely because creating a tool means submitting executable code. The update endpoint did not mirror that check, producing an asymmetric authorization surface in which a write-grantee with no workspace permission can still reach the same exec sink as a workspace.tools-trusted creator. SECURITY.md frames `workspace.tools` as the trust signal an admin uses to delegate code-execution capability; the previous behavior let that signal be bypassed by a per-resource share. Fix: after the existing ownership / write-grant / admin gate, add a content-change check. If `form_data.content != tools.content`, require `workspace.tools` or `workspace.tools_import` (or admin role). Metadata edits — `name`, `description`, valves config, access grants — continue to flow through the existing gate, so the legitimate share-for- collaboration workflow is unaffected. Reported by KadirArslan in GHSA-p4fx-23fq-jfg6 with a working three-user PoC (Alice trusted with workspace.tools creates a tool and shares write to Bob; Bob updates content and the new code runs as root inside the container, with Burp Collaborator confirming outbound exfiltration). Co-authored-by: KadirArslan * chore: trim comment --------- Co-authored-by: KadirArslan --- backend/open_webui/routers/tools.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/backend/open_webui/routers/tools.py b/backend/open_webui/routers/tools.py index 04d845c3de..3eddefdee6 100644 --- a/backend/open_webui/routers/tools.py +++ b/backend/open_webui/routers/tools.py @@ -480,6 +480,19 @@ async def update_tools_by_id( detail=ERROR_MESSAGES.UNAUTHORIZED, ) + # Content edits trigger exec on load — gate them behind workspace.tools (matches /create). + if form_data.content != tools.content: + if user.role != 'admin' and not ( + await has_permission(user.id, 'workspace.tools', request.app.state.config.USER_PERMISSIONS, db=db) + or await has_permission( + user.id, 'workspace.tools_import', request.app.state.config.USER_PERMISSIONS, db=db + ) + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + try: form_data.content = replace_imports(form_data.content) tool_module, frontmatter = await load_tool_module_by_id(id, content=form_data.content)