Reports
BAJA[AUTENTICACIÓN]#3636758

Nextcloud Talk: un participante sin permisos podía silenciar a otros inyectando señalización desde otra sala que modera

La señalización interna de Nextcloud Talk comprobaba los permisos de moderador en la sala del remitente, pero entregaba el mensaje a cualquier sesión. Un participante normal podía forzar el silencio de otro desde una sala propia.

Resumen
Resumen en castellano de un reporte público, no una traducción literal. El código y los comandos se mantienen como en el original.

Resumen

Nextcloud Talk, la app de chat y videollamadas de Nextcloud (spreed), usa por defecto una señalización interna: los clientes envían mensajes al servidor, este los guarda en una cola (tabla oc_talk_internalsignaling) y cada destinatario los recoge haciendo polling. Algunos de esos mensajes, los de tipo control (como forceMute, que silencia a un participante), están reservados a moderadores.

El fallo estaba en cómo se combinaban tres piezas:

  1. La lista de participantes revela las sesiones activas. Cualquier participante de una sala recibe el sessionId en vivo de los demás (lib/Controller/RoomController.php, líneas 1217-1220 y 1250-1253).
  2. La autorización mira la sala equivocada. En lib/Controller/SignalingController.php (líneas 387-418), el permiso de moderador para un mensaje control se comprueba contra la sala de la sesión que envía, pero el mensaje se encola para el destinatario que indique el campo to, sin comprobar que pertenezca a esa misma sala.
  3. La entrega no conoce salas. La cola solo guarda sender, recipient y message (lib/Signaling/Messages.php, líneas 48-56) y se lee únicamente por recipient = $sessionId (líneas 102-110). El polling de la víctima (SignalingController.php, líneas 551-585) consume cualquier mensaje dirigido a su sesión, venga de la sala que venga.

Resultado: un usuario que es participante normal en la sala A, pero moderador de cualquier otra sala B (basta con crearla), podía enviar desde B un forceMute dirigido a la sesión de la víctima en A. El cliente web, al recibirlo (src/utils/webrtc/simplewebrtc/simplewebrtc.js, líneas 108-113), silencia al usuario local si el peerId coincide con su sesión.

Se reprodujo en un laboratorio local con Nextcloud Server 34.0.0.1 (34.0.0 dev), Talk 24.0.0-dev.2 (commit de spreed 031e0708bde280b2a83943a0e7a6102d8eede207) y señalización interna. Se le asignó el CVE-2026-45266; HackerOne lo clasifica como Improper Authentication - Generic con severidad baja.

Pasos de reproducción

Condiciones previas: la víctima y el atacante están los dos en la sala A, el atacante solo como participante normal; el atacante es propietario o moderador de otra sala B, sin relación con A; y la señalización interna está activa.

  1. La víctima crea la sala A (token 97oqymwq), añade al atacante y ambos entran.
  2. El atacante pide la lista de participantes de la sala A y obtiene el sessionId en vivo de la víctima:
GET /ocs/v2.php/apps/spreed/api/v4/room/97oqymwq/participants?format=json
  1. El atacante crea la sala B (token pfo7kdcz), entra en ella y obtiene su propia sesión en B.
  2. Envía a la señalización de la sala B un mensaje control con forceMute cuyo destinatario (to) y peerId son la sesión de la víctima en la sala A, usando como remitente su sesión de B:
POST /ocs/v2.php/apps/spreed/api/v3/signaling/pfo7kdcz?format=json
{
  "messages": "[{\"ev\":\"message\",\"sessionId\":\"<evil_room_b_session>\",\"fn\":\"{\\\"type\\\":\\\"control\\\",\\\"to\\\":\\\"<victim_room_a_session>\\\",\\\"payload\\\":{\\\"action\\\":\\\"forceMute\\\",\\\"peerId\\\":\\\"<victim_room_a_session>\\\"}}\"}]"
}
  1. En la base de datos aparece una fila en oc_talk_internalsignaling con la sesión del atacante en B como sender y la de la víctima en A como recipient.
  2. La víctima hace su polling normal en la sala A y recibe el mensaje de control inyectado, con "from" igual a la sesión del atacante en B:
GET /ocs/v2.php/apps/spreed/api/v3/signaling/97oqymwq?format=json

Si está en una llamada, su cliente la silencia.

  1. Prueba de control: enviar el mismo forceMute desde la sala A, con la sesión del atacante en A (donde no es moderador), no encola nada para la víctima y su siguiente polling solo devuelve usersInRoom. Es decir, la comprobación de permisos funciona dentro de la sala, pero se esquiva cruzando de sala.

Impacto

  • Un usuario con pocos privilegios podía enviar controles de señalización reservados a moderadores en una sala que no modera.
  • Con forceMute, el atacante silenciaba a la víctima durante una llamada en la que el atacante no tenía ningún poder de moderación.
  • En general, la frontera entre salas de la señalización interna quedaba rota: cualquier mensaje se entregaba con solo conocer el ID de sesión del destinatario, y ese ID lo ve cualquier participante de una sala compartida.

Qué aprender de este caso

  • Si una comprobación de permisos se hace sobre el contexto del remitente (su sala, su proyecto, su organización), comprueba también que el destinatario pertenezca a ese mismo contexto antes de entregar nada; aquí faltaba verificar que la sesión indicada en to fuese de la sala desde la que se enviaba.
  • Las colas de mensajes indexadas solo por destinatario (recipient = $sessionId) son un buen sitio donde buscar fallos parecidos: guardar también la sala o el canal y filtrar por él al leer impide que un mensaje cruce de un espacio a otro.
  • Exponer identificadores de sesión en vivo a otros participantes los convierte en direcciones de ataque; si un ID sirve para enrutar mensajes, trátalo como dato sensible y no lo incluyas en listados que no lo necesitan.
  • Como prueba rápida en cualquier sistema de señalización o websockets con roles por sala: crea una sala propia donde seas moderador y envía desde ella acciones privilegiadas dirigidas a usuarios de otra sala.