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:
- La lista de participantes revela las sesiones activas. Cualquier participante de una sala recibe el
sessionIden vivo de los demás (lib/Controller/RoomController.php, líneas 1217-1220 y 1250-1253). - La autorización mira la sala equivocada. En
lib/Controller/SignalingController.php(líneas 387-418), el permiso de moderador para un mensajecontrolse comprueba contra la sala de la sesión que envía, pero el mensaje se encola para el destinatario que indique el campoto, sin comprobar que pertenezca a esa misma sala. - La entrega no conoce salas. La cola solo guarda
sender,recipientymessage(lib/Signaling/Messages.php, líneas 48-56) y se lee únicamente porrecipient = $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.
- La víctima crea la sala A (token
97oqymwq), añade al atacante y ambos entran. - El atacante pide la lista de participantes de la sala A y obtiene el
sessionIden vivo de la víctima:
GET /ocs/v2.php/apps/spreed/api/v4/room/97oqymwq/participants?format=json
- El atacante crea la sala B (token
pfo7kdcz), entra en ella y obtiene su propia sesión en B. - Envía a la señalización de la sala B un mensaje
controlconforceMutecuyo destinatario (to) ypeerIdson 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>\\\"}}\"}]"
}
- En la base de datos aparece una fila en
oc_talk_internalsignalingcon la sesión del atacante en B comosendery la de la víctima en A comorecipient. - 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.
- Prueba de control: enviar el mismo
forceMutedesde 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 devuelveusersInRoom. 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
tofuese 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.