From c05de13b4fca1ac8a17153782b46b3d0aacf491c Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Mon, 27 Jul 2026 07:51:01 +0200 Subject: [PATCH] fix: do not expose tool source code to read-only users (#27005) * fix: do not expose tool source code to read-only users The tool read endpoints build their responses from a content-bearing model via model_dump() under ConfigDict(extra='allow'). ToolResponse deliberately omits content (the Python source) and specs, but extra='allow' re-admits both, and the get_tools defer_content flag was a no-op, so GET /tools/, GET /tools/list and GET /tools/id/{id} returned a tool's full source to any caller with mere read access, including any authenticated user for a publicly read-shared tool. Tool source commonly embeds hard-coded credentials and internal URLs. Strip content and specs for callers without write access across the three read endpoints. Tool execution loads source server-side, so tool use is unaffected, and writers still receive content where they did before. The duplicated write-access check is extracted into a small helper. Co-authored-by: bogdancherniy11-sudo <229690748+bogdancherniy11-sudo@users.noreply.github.com> * fix: limit the tool source strip to the per-id endpoint Upstream dev has since fixed the defer_content no-op in Tools.get_tools, so the list endpoints (GET /tools/ and GET /tools/list) no longer fetch tool source at all and the stripping added there is redundant. Stripping specs also broke the chat Available Tools modal, which lists a tool's functions from specs for every user who can use the tool. Reduce the change to the one remaining leak: GET /tools/id/{id} builds its response from a full model_dump() and ConfigDict(extra='allow') re-admits content, so drop content there for callers without write access. Specs stay visible to read users as before and the helper functions are no longer needed. --------- Co-authored-by: bogdancherniy11-sudo <229690748+bogdancherniy11-sudo@users.noreply.github.com> --- backend/open_webui/routers/tools.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/backend/open_webui/routers/tools.py b/backend/open_webui/routers/tools.py index b8ef51a280..f14b89d6ad 100644 --- a/backend/open_webui/routers/tools.py +++ b/backend/open_webui/routers/tools.py @@ -446,20 +446,22 @@ async def get_tools_by_id(id: str, user=Depends(get_verified_user), db: AsyncSes db=db, ) ): - return ToolAccessResponse( - **tools.model_dump(), - write_access=( - (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) - or user.id == tools.user_id - or await AccessGrants.has_access( - user_id=user.id, - resource_type='tool', - resource_id=tools.id, - permission='write', - db=db, - ) - ), + write_access = ( + (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) + or user.id == tools.user_id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='tool', + resource_id=tools.id, + permission='write', + db=db, + ) ) + data = tools.model_dump() + if not write_access: + # extra='allow' re-admits content from model_dump; source is writer-only + data.pop('content', None) + return ToolAccessResponse(**data, write_access=write_access) else: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED,