fix: gate chat_completion channel: branch on channel access + message scoping (#24725)

* fix: gate chat_completion channel: branch on channel access + message scoping

When chat_id starts with 'channel:' the chat-completion handler skips
the chat ownership / storage block below it. Nothing replaced that
gate. The downstream channel emitter in socket/main.py:_make_channel_
emitter writes to Messages.update_message_by_id using a caller-supplied
message_id pulled from form_data['id'], with no membership check, no
write-access check, and no validation that the message_id belongs to
the channel.

Net effect: any authenticated user could submit
chat_id='channel:<any-channel-uuid>' + id='<any-message-uuid>' and
overwrite that message with attacker-controlled LLM output. Cross-
channel writes worked too — private channels, DMs, channels the
caller has no access to. Original author attribution stayed intact on
the overwritten row.

Add the missing checks at the channel: branch:

1. Channel must exist (404 otherwise).
2. Non-admin caller must have write access to the channel — membership
   for group/dm channels, AccessGrants permission='write' for others.
3. The message_id (if supplied) must belong to the same channel — a
   caller with write access to channel A cannot use this path to
   overwrite a message in channel B.

Behaviour change is limited to callers who were exploiting the gap:
legitimate flows that supply a message_id under their own channel
membership continue to work unchanged.

Co-authored-by: sfwani <sfwani@users.noreply.github.com>

* chore: trim verbose comment on channel: branch gate

---------

Co-authored-by: sfwani <sfwani@users.noreply.github.com>
This commit is contained in:
Classic298
2026-06-01 22:37:32 +02:00
committed by GitHub
parent 7f7cd21018
commit 33e4e0dcc4
+41
View File
@@ -470,8 +470,11 @@ from open_webui.env import (
WEBUI_SESSION_COOKIE_SECURE,
)
from open_webui.internal.db import ScopedSession, engine, get_async_session
from open_webui.models.access_grants import AccessGrants
from open_webui.models.channels import Channels
from open_webui.models.chats import ChatForm, Chats
from open_webui.models.functions import Functions
from open_webui.models.messages import Messages
from open_webui.models.models import Models
from open_webui.models.users import UserModel, Users
from open_webui.routers import (
@@ -1802,6 +1805,44 @@ async def chat_completion(
if metadata.get('chat_id') and user:
chat_id = metadata['chat_id']
# Gate channel: branch — caller needs write access on the channel
# and the supplied message_id must belong to that channel.
if chat_id.startswith('channel:'):
channel_id = chat_id.removeprefix('channel:')
channel = await Channels.get_channel_by_id(channel_id)
if not channel:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
if user.role != 'admin':
if channel.type in ['group', 'dm']:
if not await Channels.is_user_channel_member(channel.id, user.id):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.DEFAULT(),
)
else:
if not await AccessGrants.has_access(
user_id=user.id,
resource_type='channel',
resource_id=channel.id,
permission='write',
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.DEFAULT(),
)
target_message_id = list(message_ids.values())[0] if message_ids else None
if target_message_id:
target_message = await Messages.get_message_by_id(target_message_id)
if target_message and target_message.channel_id != channel.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.DEFAULT(),
)
if not chat_id.startswith('local:') and not chat_id.startswith(
'channel:'
): # temporary/channel chats are not stored