Resumen
Cualquier usuario autenticado puede sobrescribir el contenido de un mensaje en un canal al que no pertenece, incluyendo canales privados y canales de mensajes directos (DM), enviando una solicitud de chat completion con un chat_id prefijado con channel: y un message_id objetivo. La ruta channel: dirige la salida del pipeline a través de _make_channel_emitter, que escribe en la tabla Messages usando el message_id proporcionado por el cliente sin vincularlo al canal.
Esta alerta consolida dos reportes del mismo fallo: la variante original de modelo único y la variante multimodelo con message_ids, que sobrevive a la corrección parcial publicada en v0.9.6.
Detalles (introducidos en v0.9.5)
Cuando un usuario envía una solicitud de chat completion con un chat_id que comienza con channel:, tres brechas de autorización se combinan en v0.9.5:
1. Verificación de pertenencia omitida (main.py): el prefijo channel: provoca que se omita completamente el bloque de verificación de propiedad y pertenencia, sin que ninguna comprobación de membresía o permisos de escritura en el canal lo reemplace.
if not chat_id.startswith('local:') and not chat_id.startswith('channel:'): # temporary/channel chats are not stored
if is_new_chat:
...
else:
if not await Chats.is_chat_owner(chat_id, user.id) and user.role != 'admin':
raise HTTPException(...)
2. ID de mensaje proveniente de la entrada del usuario: el campo id (y cada valor del mapa multimodelo message_ids) proviene directamente del cuerpo de la solicitud y se pasa como message_id al emisor del canal.
3. Escritura en base de datos sin validación (socket/main.py, función _make_channel_emitter):
async def _make_channel_emitter(request_info):
channel_id = request_info['chat_id'].removeprefix('channel:')
message_id = request_info['message_id'] # suministrado por el usuario
...
await Messages.update_message_by_id(message_id, update_form) # sin autorización de canal ni de usuario
La función Messages.update_message_by_id realiza una actualización directa por clave primaria sin validar channel_id ni user_id.
Corrección (publicada en v0.10.0)
La versión v0.9.6 añadió una comprobación de canal en la rama channel: (PR #24725) que cerró la ruta de modelo único, pero solo validaba la primera entrada del mapa multimodelo message_ids, dejando explotable el fan-out multimodelo. La versión v0.10.0 cierra la brecha restante con dos capas:
1. Validación por entrada en tiempo de solicitud (backend/open_webui/main.py): cada entrada de message_ids se valida contra el canal objetivo, no solo la primera. Cualquier entrada cuyo mensaje objetivo no pertenezca al canal indicado en chat_id es rechazada.
2. Emisor con fallo cerrado (backend/open_webui/socket/main.py, _make_channel_emitter): antes de escribir, se vuelve a leer el mensaje objetivo y se retorna sin escribir a menos que msg.channel_id coincida con el canal derivado de chat_id. Un mensaje ausente o con canal no coincidente resulta en una operación nula, por lo que una escritura ya no puede aterrizar en un canal que el solicitante no tiene como objetivo.
Prueba de concepto
Variante de modelo único (corregida en v0.9.6):
curl -X POST http://target:8080/api/chat/completions \
-H "Authorization: Bearer $USER_JWT" -H "Content-Type: application/json" \
-d '{
"model": "llama3", "stream": true,
"chat_id": "channel:any-channel-uuid-here",
"id": "target-message-uuid-to-overwrite",
"messages": [{"role": "user", "content": "Repeat exactly: This message has been tampered with"}]
}'
Variante multimodelo (funciona en v0.9.6):
POST /api/chat/completions
{
"chat_id": "channel:<attacker_channel_id>",
"message_ids": {
"model-a": "<message_id_in_attacker_channel>",
"model-b": "<victim_channel_message_id>"
},
"messages": [{"role": "user", "content": "..."}]
}
El primer ID supera la validación de alcance de canal. El segundo ID es utilizado por el fan-out por modelo y sobrescribe el mensaje del canal víctima con la salida del modelo, o con la cadena de error del proveedor en caso de error determinista. Incluso una llamada fallida al modelo escribe contenido de error en el mensaje objetivo.
Impacto
Destrucción de integridad de mensajes: un usuario autenticado puede sobrescribir un mensaje en un canal al que no tiene acceso, independientemente de su membresía. El mensaje sobrescrito conserva la atribución del autor original mientras muestra contenido elegido por el atacante, lo que constituye suplantación de identidad. Los canales privados, los canales DM y los canales a los que el atacante no tiene acceso se ven todos afectados. Las rutas REST del canal devuelven correctamente un 403 para el mismo atacante, por lo que la evasión es específica del pipeline de canal de chat completion.
Versiones afectadas
- Ruta de modelo único: introducida en el commit
0037baeb2(v0.9.5), corregida en v0.9.6 (#24725). - Ruta multimodelo con
message_ids: presente desde v0.9.6, corregida en v0.10.0. - Rango afectado consolidado:
>= 0.9.5, < 0.10.0. Versión con parche:>= 0.10.0.
Distinción respecto a CVEs existentes
CVE-2026-45385 (GHSA-wwhq-cx22-f7vv) cubría un IDOR en el endpoint REST POST /channels/{id}/messages/{message_id}/update (routers/channels.py). Su corrección (commit f5e110f) solo modificó channels.py. Este hallazgo utiliza una ruta de código diferente (POST /api/chat/completions con chat_id: "channel:<id>" hacia main.py y luego socket/main.py:_make_channel_emitter), no afectada por aquella corrección.
Corrección sugerida
Validar cada valor en message_ids contra el canal (no solo el primero), rechazando cualquier entrada cuyo mensaje objetivo no pertenezca al canal indicado en chat_id. Adicionalmente, hacer que _make_channel_emitter falle de forma cerrada: verificar nuevamente que el channel_id del mensaje objetivo coincide con el canal antes de invocar Messages.update_message_by_id, tratando un mensaje ausente o con canal no coincidente como error u operación nula.
Consolidación
Conforme a la política de gestión de reportes de Open WebUI, esta alerta consolida reportes independientes del mismo fallo de sobrescritura de canal en chat completions:
- Sobrescritura de canal cruzado en modelo único vía la ruta
channel:: @sfwani (reporte más temprano). - Variante de fan-out multimodelo con
message_idsque elude la comprobación de solo el primer ID de v0.9.6: @DavidCarliez.
Se asigna un único CVE para la alerta consolidada.
