From cd68a66fba0a14c757be730a57784945ca2dedb3 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sun, 6 Sep 2026 22:58:50 +0200 Subject: [PATCH] Gate the channel webhook profile image endpoint on channel access (#29703) Any authenticated user could fetch a channel webhook's avatar, or be redirected to its external profile image URL, without belonging to the channel or holding any read access to it. This was the only webhook route with neither a channel check nor the channels feature gate. The route now applies the same read gate every other route in this router uses: active membership for group and direct message channels, admin or a channel read grant otherwise, answering with 403 on denial and 404 when the webhook's channel row no longer exists. It also runs the channels feature and permission gate, so with channels disabled, or the permission withdrawn from regular users, the endpoint now refuses where it previously served the image. Avatars keep rendering for channel members, and a denied request shows the default logo rather than a broken image, because the avatar component already falls back on an image error. --- backend/open_webui/routers/channels.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/backend/open_webui/routers/channels.py b/backend/open_webui/routers/channels.py index 3c165f16ff..aa7248d561 100644 --- a/backend/open_webui/routers/channels.py +++ b/backend/open_webui/routers/channels.py @@ -1823,9 +1823,15 @@ async def delete_message_by_id( @router.get('/webhooks/{webhook_id}/profile/image') -async def get_webhook_profile_image(webhook_id: str, user=Depends(get_verified_user)): +async def get_webhook_profile_image( + request: Request, + webhook_id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): """Get webhook profile image by webhook ID.""" - webhook = await Channels.get_webhook_by_id(webhook_id) + await check_channels_access(request, user) + webhook = await Channels.get_webhook_by_id(webhook_id, db=db) if not webhook: # Return default favicon if webhook not found # LICENSE covers this Open WebUI fallback logo. @@ -1833,6 +1839,17 @@ async def get_webhook_profile_image(webhook_id: str, user=Depends(get_verified_u # https://docs.openwebui.com/license. return FileResponse(f'{STATIC_DIR}/favicon.png') + channel = await Channels.get_channel_by_id(webhook.channel_id, db=db) + if not channel: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + if channel.type in ['group', 'dm']: + if not await Channels.is_user_channel_member(channel.id, user.id, db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + else: + if user.role != 'admin' and not await channel_has_access(user.id, channel, permission='read', db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + if webhook.profile_image_url: # Check if it's url or base64 if webhook.profile_image_url.startswith('http'):