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.
This commit is contained in:
Classic298 2026-09-06 22:58:50 +02:00 committed by GitHub
parent 517617b601
commit cd68a66fba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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'):