Reports
MEDIA[OTRAS INYECCIONES]#3590586

Roundcube: «position: fixed !important» burla el saneador de CSS y permite superponer avisos falsos

El saneador de CSS de Roundcube convertía «position: fixed» en «absolute», pero no reconocía «fixed !important». Un correo HTML podía así cubrir el panel de lectura o toda la ventana con un aviso falso.

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

Roundcube, el cliente de correo web, limpia el CSS de los mensajes HTML antes de mostrarlos. Una de sus defensas, dentro de la función sanitize_css_block() de program/lib/Roundcube/rcube_utils.php, consiste en cambiar position: fixed por position: absolute. La idea es impedir que un correo fije un elemento a la ventana del navegador y la tape por completo con contenido ajeno al mensaje.

El fallo está en cómo se hace esa comprobación:

strcasecmp($value, 'fixed') === 0

La comparación exige que el valor completo, ya recortados los espacios de los lados, sea exactamente fixed. Con fixed !important la comparación falla y la conversión no se aplica. El valor pasa entonces a la validación genérica por tokens: explode_css_property_block() lo parte en ['fixed', '!important'], cada token supera por separado la lista de valores permitidos y la declaración se vuelve a ensamblar en la salida como position: fixed !important.

El reporte llegó a Roundcube a través del programa de Nextcloud en HackerOne, que lo clasifica como "Resource Injection" con severidad media.

Pasos de reproducción

  1. Enviar a la víctima un correo HTML con este cuerpo:
<!DOCTYPE html>
<html>
<head><title>Account Update</title></head>
<body>
<p>Please see below for your account details.</p>

<style>
.overlay {
    position: fixed !important;
    top: 0; left: 0;
    width: 100%; height: 100%;
    background: white;
    z-index: 99999;
    display: flex;
    align-items: center;
    justify-content: center;
}
.dialog {
    border: 1px solid #ccc;
    border-radius: 8px;
    padding: 30px;
    max-width: 400px;
    text-align: center;
    font-family: Arial, sans-serif;
}
</style>

<div class="overlay">
  <div class="dialog">
    <h2>Session Expired</h2>
    <p>Your Roundcube session has expired due to inactivity.</p>
    <p><a href="https://ATTACKER_SERVER/phish/login" style="background:#0066cc;color:white;padding:10px 20px;text-decoration:none;border-radius:4px;">Sign In Again</a></p>
  </div>
</div>

</body>
</html>
  1. Abrir el correo en Roundcube.
  2. Comprobar que el aviso falso cubre:
  • el panel de previsualización (el iframe) en la vista normal;
  • la ventana completa del navegador cuando el mensaje se abre en una ventana nueva (sin iframe).

Impacto

Inyección de CSS que permite superponer contenido arbitrario sobre la interfaz del cliente de correo y usarlo para phishing. En la prueba del investigador, el correo muestra un falso "la sesión ha caducado" a pantalla completa con un botón que lleva a un servidor controlado por el atacante. Como aparece dentro del propio Roundcube, a la víctima le cuesta distinguirlo de un aviso legítimo de la aplicación.

Qué aprender de este caso

  • Cuando una defensa compara un valor CSS con una palabra exacta (strcasecmp($value, 'fixed')), prueba qué pasa si llega acompañado de !important, comentarios /* */ o espacios intermedios: la igualdad estricta falla y el valor puede colarse por la ruta de validación genérica.
  • Al auditar saneadores de CSS/HTML, localiza las propiedades que se normalizan según su valor (position es el ejemplo aquí) y compara esa ruta con la validación general: si una acepta lo que la otra quería neutralizar, hay un bypass.
  • La decisión de seguridad debe tomarse sobre el valor ya descompuesto en tokens (o tras quitar !important), no sobre la cadena completa; si no, el propio parser posterior la deshace.
  • En cualquier sitio donde se muestre HTML de terceros, ten en cuenta que position: fixed con z-index alto basta para tapar la interfaz anfitriona: conviene neutralizarlo de forma robusta y no confiar solo en el iframe, que deja de proteger cuando el mensaje se abre fuera de él.