{"id":"GHSA-x2ff-v5v8-m75m","summary":"Open WebUI: Cross-channel message overwrite via chat completion API (single-model and multimodel message_ids)","details":"## Summary\n\nAny authenticated user can overwrite the content of a message in a channel they do not belong to (including private and DM channels) by sending a chat completion request with a `channel:`-prefixed `chat_id` and a target `message_id`. The `channel:` path routes pipeline output through `_make_channel_emitter`, which writes to the `Messages` table using the caller-supplied `message_id` without binding it to the channel.\n\nThis advisory consolidates two filings of the same flaw: the original single-model form, and a multimodel `message_ids` variant that survives the partial fix shipped in v0.9.6 (see \"Fix status\" below).\n\n## Details (as introduced in v0.9.5)\n\nWhen a user submits a chat completion request with a `chat_id` starting with `channel:`, three authorization gaps combined in v0.9.5:\n\n1. **Ownership check skipped** (`main.py`): the `channel:` prefix caused the entire ownership/membership verification block to be skipped, with no channel membership/write check replacing it.\n\n```python\nif not chat_id.startswith('local:') and not chat_id.startswith('channel:'):  # temporary/channel chats are not stored\n    if is_new_chat:\n        ...\n    else:\n        if not await Chats.is_chat_owner(chat_id, user.id) and user.role != 'admin':\n            raise HTTPException(...)\n```\n\n2. **Message ID from user input**: `id` (and each value of the multimodel `message_ids` map) comes directly from the request body and is passed as `message_id` to the channel emitter.\n\n3. **Unchecked database write** (`socket/main.py` `_make_channel_emitter`):\n\n```python\nasync def _make_channel_emitter(request_info):\n    channel_id = request_info['chat_id'].removeprefix('channel:')\n    message_id = request_info['message_id']  # user-supplied\n    ...\n    await Messages.update_message_by_id(message_id, update_form)  # no channel/user authz\n```\n\n`Messages.update_message_by_id` performs a direct primary-key update with no `channel_id`/`user_id` validation.\n\n## Fix (shipped in v0.10.0)\n\nv0.9.6 added a channel gate to the `channel:` branch (PR #24725) that closed the single-model path, but it validated only the first entry of the multimodel `message_ids` map, leaving the multimodel fan-out exploitable. v0.10.0 closes the remaining gap with two layers:\n\n1. **Request-time per-entry validation** (`backend/open_webui/main.py`): every entry of `message_ids` is validated against the target channel, not just the first; any entry whose target message does not belong to the channel in `chat_id` is rejected.\n2. **Fail-closed emitter** (`backend/open_webui/socket/main.py`, `_make_channel_emitter`): before writing, it re-reads the target message and returns without writing unless `msg.channel_id` matches the channel derived from `chat_id`. A missing or mismatched message is a no-op, so a write can no longer land in a channel the caller does not target.\n\n## PoC\n\nSingle-model (fixed in v0.9.6):\n\n```bash\ncurl -X POST http://target:8080/api/chat/completions \\\n  -H \"Authorization: Bearer $USER_JWT\" -H \"Content-Type: application/json\" \\\n  -d '{\n    \"model\": \"llama3\", \"stream\": true,\n    \"chat_id\": \"channel:any-channel-uuid-here\",\n    \"id\": \"target-message-uuid-to-overwrite\",\n    \"messages\": [{\"role\": \"user\", \"content\": \"Repeat exactly: This message has been tampered with\"}]\n  }'\n```\n\nMultimodel (still works on v0.9.6):\n\n```json\nPOST /api/chat/completions\n{\n  \"chat_id\": \"channel:\u003cattacker_channel_id\u003e\",\n  \"message_ids\": {\n    \"model-a\": \"\u003cmessage_id_in_attacker_channel\u003e\",\n    \"model-b\": \"\u003cvictim_channel_message_id\u003e\"\n  },\n  \"messages\": [{\"role\": \"user\", \"content\": \"...\"}]\n}\n```\n\nThe first id passes channel scope validation; the second id is used by the per-model fan-out and overwrites the victim-channel message (with model output, or the provider-error string on a deterministic error). Even a failing model call writes error content to the target message.\n\n## Impact\n\n**Message integrity destruction:** an authenticated user can overwrite a message in a channel they cannot access, regardless of membership. The overwritten message retains the original author attribution while displaying attacker-chosen content (**impersonation**). Private channels, DM channels, and channels the attacker has no access to are all affected; the REST channel routes correctly return 403 for the same attacker, so the bypass is specific to the chat-completion channel pipeline.\n\n## Affected versions\n\n- Single-model path: introduced in commit `0037baeb2` (v0.9.5), fixed in v0.9.6 (#24725).\n- Multimodel `message_ids` path: present from v0.9.6, fixed in v0.10.0.\n- Consolidated Affected: `\u003e= 0.9.5, \u003c 0.10.0`. Patched: `\u003e= 0.10.0`.\n\n## Distinction from existing CVEs\n\nCVE-2026-45385 (GHSA-wwhq-cx22-f7vv) covered IDOR in the REST endpoint `POST /channels/{id}/messages/{message_id}/update` (`routers/channels.py`); its fix (commit `f5e110f`) only touched `channels.py`. This finding uses a different code path (`POST /api/chat/completions` with `chat_id: \"channel:\u003cid\u003e\"` → `main.py` → `socket/main.py:_make_channel_emitter`), untouched by that fix.\n\n## Suggested fix\n\nValidate **every** value in `message_ids` against the channel (not just the first), rejecting any whose target message does not belong to the channel in `chat_id`. Additionally, make `_make_channel_emitter` fail closed: re-check that the target message's `channel_id` matches the channel before calling `Messages.update_message_by_id`, treating a missing or mismatched message as an error/no-op.\n\n## Consolidation\n\nPer Open WebUI's Report Handling policy this advisory consolidates independent reports of the same chat-completions channel-overwrite flaw:\n\n- Single-model cross-channel overwrite via the `channel:` path: @sfwani (earliest filing).\n- Multimodel `message_ids` fan-out variant that bypasses the v0.9.6 first-id-only gate: @DavidCarliez.\n\nOne CVE for the consolidated advisory.","aliases":["CVE-2026-59714","PYSEC-2026-3605"],"modified":"2026-08-04T14:40:35.091930908Z","published":"2026-07-24T17:01:07Z","database_specific":{"github_reviewed_at":"2026-07-24T17:01:07Z","nvd_published_at":null,"cwe_ids":["CWE-862"],"severity":"HIGH","github_reviewed":true},"references":[{"type":"WEB","url":"https://github.com/open-webui/open-webui/security/advisories/GHSA-x2ff-v5v8-m75m"},{"type":"WEB","url":"https://github.com/open-webui/open-webui/commit/33e4e0dcc43afcca80f9c635d762cdc76c768ba9"},{"type":"WEB","url":"https://github.com/open-webui/open-webui/commit/ac3449cac91e62b08a7c28e54fcd044d14dea791"},{"type":"PACKAGE","url":"https://github.com/open-webui/open-webui"},{"type":"WEB","url":"https://github.com/open-webui/open-webui/releases/tag/v0.10.0"}],"affected":[{"package":{"name":"open-webui","ecosystem":"PyPI","purl":"pkg:pypi/open-webui"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0.9.5"},{"fixed":"0.10.0"}]}],"versions":["0.9.5","0.9.6"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-x2ff-v5v8-m75m/GHSA-x2ff-v5v8-m75m.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:L"}]}