From fabee774ee104676f5aac4fbe2ec70447370ee09 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Thu, 11 Jun 2026 13:04:43 +0200 Subject: [PATCH] Stop /channels/{id}/members from leaking members' private fields (CWE-200) get_channel_members_by_id returned the full UserModel for every channel member via UserModelResponse(**u.model_dump(), ...), including settings (which holds per-user tool-server bearer keys and webhook URLs), oauth, scim, info and PII (date_of_birth/gender/bio). Channel membership is self-grantable (any user can open a DM with any other user), so a low-privilege user could DM an admin and read the admin's tool-server API keys from the members list. Exclude those fields from both members response paths (DM and group/non-DM) via a shared _CHANNEL_MEMBER_PRIVATE_FIELDS set. The response shape is unchanged; the sensitive fields are simply omitted. Sibling endpoints that embed users already use the minimal UserIdNameStatusResponse and are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/open_webui/routers/channels.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/backend/open_webui/routers/channels.py b/backend/open_webui/routers/channels.py index 11d3a4a871..ae2848b7f0 100644 --- a/backend/open_webui/routers/channels.py +++ b/backend/open_webui/routers/channels.py @@ -440,6 +440,11 @@ async def get_channel_by_id( PAGE_ITEM_COUNT = 30 +# UserModel fields that must not leak to other channel members: credentials / integration config +# (settings holds tool-server bearer keys + webhook URLs), identity-provider data, and PII. +_CHANNEL_MEMBER_PRIVATE_FIELDS = {'settings', 'oauth', 'scim', 'info', 'date_of_birth', 'gender', 'bio'} + + @router.get('/{id}/members', response_model=UserListResponse) async def get_channel_members_by_id( request: Request, @@ -475,7 +480,7 @@ async def get_channel_members_by_id( total = len(fetched_users) return { - 'users': [UserModelResponse(**u.model_dump(), is_active=Users.is_active(u)) for u in fetched_users], + 'users': [UserModelResponse(**u.model_dump(exclude=_CHANNEL_MEMBER_PRIVATE_FIELDS), is_active=Users.is_active(u)) for u in fetched_users], 'total': total, } else: @@ -503,7 +508,7 @@ async def get_channel_members_by_id( total = result['total'] return { - 'users': [UserModelResponse(**u.model_dump(), is_active=Users.is_active(u)) for u in fetched_users], + 'users': [UserModelResponse(**u.model_dump(exclude=_CHANNEL_MEMBER_PRIVATE_FIELDS), is_active=Users.is_active(u)) for u in fetched_users], 'total': total, }