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) <noreply@anthropic.com>
This commit is contained in:
Classic298 2026-06-11 13:04:43 +02:00
parent b1d40f3409
commit fabee774ee

View file

@ -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,
}