Summary On standard channels, the message update and delete handlers accepted any caller holding write access on the channel, without checking that the caller wrote the message. Write access is the same grant a member needs in order to post, so every ordinary participant in a shared channel could rewrite or permanently delete any other participant's messages. The group and direct message branch of the same handlers enforced authorship; the standard branch did not.
Preconditions Channels are disabled by default and must be enabled by an administrator (ENABLECHANNELS). The channel must be a standard channel; group and direct message channels are not affected. The attacker is any authenticated account with role user that holds write access on the channel, whether granted publicly, per user, or through a group. No ownership of the channel, channel manager role, or elevated role is required. Channel and message ids are returned by the listing endpoints the member can already call.
Impact An ordinary member could replace the content of another member's message while the message stayed attributed to its original author, and could attach arbitrary data and meta payloads to it through the same form. The member could also permanently delete other members' messages, up to the entire visible history of the channel. This is an integrity and availability loss against other users of the channel: content can be forged under a colleague's name and records can be destroyed. It reaches no further than channels the attacker already has write access to, and it discloses nothing the attacker could not already read as a member. Pinning is unaffected and is not part of this issue.
Fix Fixed in c609ec411 (#27197), released in 0.11.0. Both handlers now apply the same authorship check the group and direct message branch already used, so a non-admin caller can act only on their own messages regardless of write access. Upgrading fully resolves it, with no configuration change required.
Root cause Affected components: - backend/openwebui/routers/channels.py, updatemessagebyid (POST /api/v1/channels/{id}/messages/{messageid}/update) - backend/openwebui/routers/channels.py, deletemessagebyid (DELETE /api/v1/channels/{id}/messages/{messageid}/delete)
Both handlers branch on channel type, and the two branches asked different questions. The group and direct message branch asked whether the caller wrote the message. The standard branch asked whether the caller is allowed to write in the channel, which is a permission level, not an identity. Those are not interchangeable: posting a message runs that same write check, so the grant that lets a member participate was silently accepted as the grant to rewrite and remove everyone else's content, and every ordinary participant satisfied it. The model layer looks messages up by primary key alone, so the router branch was the only authorization that ran.
Proof of concept Reported with a script that runs end to end against a live instance. An administrator seeds the accounts and a standard channel granting read and write; everything after that is performed by a separate plain user account in no group that owns neither the channel nor any of the messages. That account edits a victim's message, attaches structured payloads to it, and deletes a second victim message, all returning 200, with the channel history dumped before and after. Controls in the same run confirm the scoping: a read-only member is refused with 403, and a cross-member edit on a group channel is refused with 403.
Credits @Foxer131 — reported the missing authorship check on the standard-channel update and delete handlers.
Summary Chat histories are stored as an unvalidated JSON object. The walk that reconstructs a chat's message chain detected repeats using each message's own id field while moving through the history by map key, so a message that simply omitted id was never recorded as visited. A history whose messages referenced each other in a parent cycle therefore made the walk run forever. Any account with the default user role could store such a chat and trigger the walk, blocking the server for everyone.
Preconditions One account with the default user role. No administrator rights, no additional permissions, no configuration change and no non-default setting: creating a chat is available to every user out of the box. The attack runs entirely against the attacker's own chat, so no knowledge of any other user's data is needed. Instances where every account is trusted are affected in the sense that the fault is reachable, but require a user acting deliberately.
Impact The walk is synchronous and runs on the asyncio event loop, so while it spins, every request from every user is blocked, including unauthenticated /health and administrator endpoints. External health checks and orchestrator liveness probes fail alongside the UI. The list it appends to grows without bound, so a memory-capped deployment ends in an out-of-memory kill rather than a hang. The work is not cancelled when the client disconnects, so one fire-and-forget request is enough and the attacker can disconnect immediately. The malformed chat stays in the database, so restarting the process does not clear the condition: the next request that walks that chat hangs the new process, and recovery requires deleting the stored chat. No data is disclosed, altered or deleted.
Fix Fixed in 0.11.1 by https://github.com/open-webui/open-webui/commit/5c79ccc9e5c9efc2bc024d8f0b9757652ece929a. The walk now records the map key it is currently positioned at instead of the message's self-reported id, so it terminates after at most one step per stored message whatever the message contents are. Upgrading fully resolves the issue, including for chats stored while the deployment was on an affected version, which become harmless once the walk terminates.
Root cause Affected component: the message-chain reconstruction helper in backend/openwebui/utils/misc.py, reached from every path that rebuilds a chat's history, including chat completion, per-chat statistics, context compaction, subagents and timers. Affected setup: all builds from 0.5.0 up to and including 0.11.0.
The loop's visited set was keyed on the message body's id field while the loop itself advanced by looking the parent up as a key in the history map, so the two used different notions of identity. That id field is part of the stored chat object and is fully attacker-controlled, and the guard explicitly skipped recording it when absent, which left the exit condition unreachable for any message that omitted it. The write path does not validate the structure of a chat's history, so a history containing id-less messages in a parent cycle was persisted exactly as submitted.
Proof of concept As a default-role user on a running instance, store one chat of two messages that reference each other as parents, with the id field omitted:
json {"chat":{"title":"poc","history":{"currentId":"A","messages":{ "A":{"parentId":"B","role":"user","content":"a","childrenIds":[]}, "B":{"parentId":"A","role":"assistant","content":"b","childrenIds":[]}}}}}
POST /api/v1/chats/new stores it verbatim. A single subsequent GET /api/v1/chats/stats/usage as the same user then never returns. Sampled during the hang, the worker consumed one full CPU core and reached 1.86 GB resident within 60 seconds, still growing. Unauthenticated GET /health and administrator API calls both time out for as long as the process lives, and continue to do so after the attacker's connection closes. Restarting the server restores service until the first request that walks the stored chat, which hangs the new process the same way.
On 0.11.1 the same payload returns promptly, /health stays available throughout, and a well-formed chat still resolves its full history.
Credits @YashvantHange, who reported the missing-id cycle in the message-chain walk, demonstrated the resulting server-wide outage end to end against a live instance, and showed that it survives both attacker disconnect and a process restart.