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>
This commit is contained in:
Classic298
2026-07-27 07:51:01 +02:00
committed by GitHub
parent 9562f1a67d
commit c05de13b4f
+15 -13
View File
@@ -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,