From f5e110fbee55c320b066c014573342781fcbd634 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sun, 10 May 2026 18:03:39 +0200 Subject: [PATCH] fix: enforce message ownership in group/DM channel update + delete endpoints (#24506) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: enforce message ownership in group/DM channel update + delete endpoints `update_message_by_id` (channels.py:1348) and `delete_message_by_id` (channels.py:1550) branch on `channel.type`. The `else` branch (standard channels) correctly enforces `message.user_id != user.id` ownership before mutating, but the `if channel.type in ['group', 'dm']` branch only checked `is_user_channel_member` — channel membership alone, with no message ownership verification. Effect on group/DM channels: any verified member of the conversation could: - overwrite another member's message content while the server preserved `user_id=victim`, producing tampered content that renders to other members as the original author's authentic post (integrity + authenticity); - silently delete another member's messages, removing them from conversation history without trace (integrity). Reproduced end-to-end against v0.9.4 with three users (attacker, victim, viewer) sharing a group channel: attacker overwrites victim's message and deletes another, viewer reads the tampered content as victim-authored. Two patches, identical shape, mirror the `else` branch's existing ownership semantics: - `update_message_by_id` group/DM branch: add `if user.role != 'admin' and message.user_id != user.id: raise 403` immediately after the `is_user_channel_member` check. - `delete_message_by_id` group/DM branch: same. The standard-channel branch is unchanged (it already enforced ownership). Admins remain able to moderate any message, matching the existing semantic in the standard-channel branch. Reports consolidated under GHSA-wwhq-cx22-f7vv (earliest live filing of the group/DM-specific variant). Same gap previously surfaced and partially fixed under GHSA-jxwr-g6r6-j3fx (which addressed the standard-channel branch only) — this completes the cohort. * chore: trim comments --- backend/open_webui/routers/channels.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/open_webui/routers/channels.py b/backend/open_webui/routers/channels.py index 7c2ab1ce69..3c7fef8773 100644 --- a/backend/open_webui/routers/channels.py +++ b/backend/open_webui/routers/channels.py @@ -1369,6 +1369,9 @@ async def update_message_by_id( 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()) + # Membership is not authorship — block cross-member edits. + if user.role != 'admin' and message.user_id != user.id: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) else: if ( user.role != 'admin' @@ -1570,6 +1573,9 @@ async def delete_message_by_id( 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()) + # Membership is not authorship — block cross-member deletes. + if user.role != 'admin' and message.user_id != user.id: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) else: if ( user.role != 'admin'