Skip to main content

Cómo bloquear un servidor de correo electrónico con un solo correo

Escrito por

Joran Greef

crash an email server with single email small

1 de agosto de 2018

0 minutos de lectura

Recientemente se descubrió que cinco de los analizadores de correo electrónico más populares para Node.js son vulnerables a un ataque trivial de denegación de servicio (DoS). La vulnerabilidad se puede explotar al incluir varios millones de archivos adjuntos vacíos en un correo electrónico que eluda los límites de tamaño habituales (generalmente de 20 MB o menos). Cuando se envía el correo a un servidor de correo vulnerable, el bucle de eventos de Node.js se congela durante varios segundos debido a la enorme cantidad de archivos adjuntos. El uso de memoria se dispara a 2 GB o más debido a los objetos internos que se crean para cada archivo adjunto, lo que suele bastar para que todo el servidor se caiga por falta de memoria. Entonces, ¿tu servidor Node.js analiza correos electrónicos? ¿Sabes qué analizador de correo estás usando? Antes de revisarlo, veamos quiénes se ven afectados.

Antes de continuar, aquí está el XKCD obligatorio.

Correo escrito a mano para disculparse con Kevin por una demora de dos años, hablar de la ansiedad que generan los correos y rechazar una invitación de LinkedIn, junto a una persona que usa una laptop.

Un ataque de denegación de servicio no debería ser tan fácil, ¿verdad?

La vulnerabilidad es fácil de explicar y de explotar, y afecta a miles de sistemas. La biblioteca mailparser, por ejemplo, recibe hasta 249,400 descargas mensuales y es una dependencia de otros 214 proyectos, incluido Sendgrid. Haraka es otra biblioteca afectada que ha sido utilizada por Craigslist, Fort Anti-Spam y ThreatWave.

La solución es una línea de código. No necesitas Cloudflare. Solo tienes que validar los datos de los usuarios. Esto se puede hacer contando la cantidad de archivos adjuntos (incluidas las partes de texto) y reaccionando si el número supera, por ejemplo, los 1000. ¿Cuándo fue la última vez que viste un correo con 10,000 archivos adjuntos? ¿Cuándo necesitaste enviar 100,000 archivos adjuntos? Y si sigues analizando después de un millón de archivos adjuntos… bueno, ¡sabes que te excediste!

Espera, ¿cómo se nos pasó esto? Si es tan fácil de corregir como decimos, ¿cómo es que al menos cinco implementaciones lo hicieron mal? Además, ¿cómo encontramos la vulnerabilidad y qué podemos hacer para mejorar?

Imagina que estás escribiendo un analizador de correo electrónico…

Sabes cuántos RFC tienes que leer (e interpretar). Sabes cuántas pruebas tienes que escribir para asegurarte de cumplir lo mejor posible con los RFC. Has escuchado el mantra del software: «primero haz que funcione y luego haz que sea rápido». Pero los analizadores de correo son difíciles, y terminarás conformándote con que simplemente funcionen. Cuando escribes uno, empiezas a entender por qué quizá no hagas un rápido cálculo aproximado para estimar cuánta memoria podría asignarse a un solo objeto multiparte de tu diseño. Si haces un análisis de complejidad:

Es probable que midas solo el uso de CPU, no el de memoria.Probablemente no evalúes la huella de memoria típica.Terminas siendo indiferente al entorno SMTP, así que no intentas optimizar el análisis con rutas rápidas, aunque el 90 % de los correos analizados sean spam.Evitas aplicar demasiadas políticas estrictas en tu analizador.Es posible que a tus usuarios no les guste que limites la cantidad máxima de archivos adjuntos por correo.Preferirías analizarlo todo y dejar que el usuario rechace el correo durante la transacción SMTP si es necesario. Al fin y al cabo, tú no eres el administrador del servidor de correo.

Imagina que administras un servidor de correo…

Una de las primeras cosas que probablemente harás es decidir cuál será el límite de tamaño de los correos. Cuanto más grande sea el correo, menos probable será que otros servidores lo acepten. Fijas el límite de tamaño en tan solo 20 MB, pensando que eso también mantendrá el tiempo de análisis dentro de límites razonables. Decides usar un analizador de correo popular y probado en batalla. Confías en que la complejidad del analizador sea lineal, o O(N), en relación con el tamaño del correo. Evalúas el uso de CPU con correos del tamaño máximo de 20 MB y esperas que tu servidor procese miles de mensajes por segundo. Esperas que todos los correos de 20 MB tarden aproximadamente lo mismo. Calculas que tan solo 8 GB de RAM deberían bastar para procesar 200 correos simultáneos de 20 MB por segundo, ya que no esperas que tu base de usuarios crezca tan rápido. Si alguien apostara contigo a que tu servidor puede procesar 10 correos simultáneos de 20 MB, estarías seguro de ganar. Lo último que se te ocurriría sería un archivo adjunto de 0 bytes. ¿Qué daño ha causado alguna vez un archivo vacío?

Y entonces ves esto:

MIME-Version: 1.0
From: <trusting@user.data>
To: <validating@user.data>
Subject: MIME Multipart Attack
Date: Sat, 30 Jun 2018 15:51:58 +0000
Message-ID: <allocate_gigabytes_of_ram@node.js>
Content-Type: multipart/mixed; boundary="0"

--0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: quoted-printable

--0
--0
--0
--0
--0
--0
--0
--0 [× 4 million]</allocate_gigabytes_of_ram@node.js></validating@user.data></trusting@user.data>

¿Cómo encontramos la vulnerabilidad?

Ronomon es una startup de correo electrónico que está en beta privada. Cuando empecé, tuve un desagradable encuentro con correos entrantes que hacían que el recolector de basura de V8 bloqueara el bucle de eventos de Node.js durante decenas de segundos. Pasé dos días de mis vacaciones alternando indicadores de funciones cada pocos minutos para reducir la carga mientras intentaba entender qué ocurría. Finalmente, con la ayuda de Vyacheslav Egorov, comenté el funcionamiento interno de la función CollectAllAvailableGarbage de V8, que realizaba alegremente siete ciclos de recolección arbitrarios en un montón enorme de varios gigabytes. Mirando atrás, fue una gran experiencia de aprendizaje. Ahora tengo mucho cuidado al asignar objetos en el heap y al bloquear el bucle de eventos.

A principios del año pasado, me propuse escribir un nuevo analizador de correo, que desde entonces se publicó como código abierto con el nombre @ronomon/mime. El objetivo era que fuera 10× más rápido que el analizador anterior, con la menor cantidad posible de asignaciones, que funcionara con búferes sin procesar y cumpliera con los RFC, con una cobertura de pruebas del 100 %, incluidas pruebas fuzz. Fue difícil lograrlo, y requirió hacer cosas como eliminar las conversiones de búfer a cadena y reducir las predicciones erróneas de saltos causadas por el Base64 dividido en líneas de 78 caracteres.

En el proceso, aprendí que algunas decisiones de políticas quizá sea mejor tomarlas en el analizador de correo que en el servidor, y viceversa. Entre ellas, rechazar codificaciones Base64, Quoted-Printable o de caracteres evidentemente maliciosas, dañadas o truncadas; rechazar duplicados de encabezados críticos; limitar la cantidad de partes multiparte y limitar el retroceso causado por límites multiparte que generan falsos positivos.

A principios de este año, me puse en contacto con Jamie Davis, quien escribió una excelente guía para evitar bloquear el bucle de eventos de Node.js. Jamie estaba haciendo investigaciones sobre ataques al bucle de eventos y le sugerí que algunos analizadores de correo podrían ser vulnerables a un ataque multiparte. Me sorprendió que la hipótesis se confirmara en todos los analizadores de correo para Node.js que probé.

Diez cosas que podríamos hacer mejor

Aquí tienes diez consejos y buenas prácticas que deberías tener en cuenta:

  1. Los ataques DoS explotan la escasez de recursos. Practica la empatía mecánica y recuerda que estás escribiendo para una máquina. Valora los recursos mecánicos que te han confiado. Adminístralos bien. No desperdicies, no te faltará. No te limites a lograr que el uso de CPU sea O(N): procura que las asignaciones de memoria también sean O(N), al igual que cualquier otro recurso que utilices. Si tu código es eficiente en cuanto a CPU, memoria, disco y red, será menos vulnerable al agotamiento de recursos y tendrás más probabilidades de establecer límites razonables para todos los recursos de tu sistema.

  2. Haz cálculos aproximados de todas las dimensiones de recursos desde el comienzo del diseño. Así detectarás antes los diseños deficientes y evitarás intentar implementaciones «imposibles». El rendimiento y la seguridad no son aspectos que puedas optimizar ni incorporar después. Debes contemplarlos desde el principio. No esperes a que tu módulo se vuelva popular.

  3. Equilibra el uso de recursos en todas las dimensiones. Quizá tengas suficiente CPU para alcanzar tus objetivos de rendimiento, pero ¿te quedarás sin memoria antes? Una vez más, necesitas hacer cálculos aproximados para mantener el uso proporcional entre las distintas dimensiones de recursos y evitar cuellos de botella en tu diseño.

  4. Recuerda que un problema de rendimiento no es más que un DoS a punto de ocurrir. En especial, cuando usas un bucle de eventos. La próxima vez que un usuario reporte un problema de rendimiento, considéralo una oportunidad para prevenir un problema de seguridad.

  5. Valida todos los datos de los usuarios, no solo «cuánto», sino también «cuántos». De hecho, a menudo debes prestar atención a las cosas pequeñas, porque suelen aparecer con más frecuencia y pueden multiplicarse y amplificarse en tu contra.

  6. Pregúntate siempre: ¿qué considero razonable? No permitas nada que supere 10× tu umbral razonable. Incorpora tus expectativas en el código que escribes.

  7. Presta atención a las brechas entre los límites de los módulos. No des por sentado que «alguien más se encargará». No dejes que las decisiones de políticas queden en el olvido. Quizá tengas que entender mejor tus dependencias.

  8. Trata los datos evidentemente incorrectos como si fueran tóxicos. No los toques ni con un palo de tres metros. Deshazte de ellos lo antes posible.

  9. Pregúntate: ¿qué haría un usuario malicioso? No te limites a revisar tu código. Intenta explotarlo activamente. Antes de publicar, piensa en al menos tres formas de explotar cada módulo y soluciónalas. Ponte una meta y encuéntralas. Siempre están ahí. Te sorprenderás.

  10. Las pruebas fuzz tienen una imaginación fantástica. Escribe tus propias pruebas fuzz sencillas para generar un espectro aleatorio de argumentos válidos e inválidos. Para comprobar la corrección, compara los valores de retorno de tu función con otra implementación cuando los argumentos sean válidos, y verifica que se produzcan excepciones cuando no lo sean. Las pruebas fuzz que ejecutan millones de permutaciones de argumentos son como la ley de Linus llevada al extremo: simulan en solo unos segundos la capacidad de miles de ojos para encontrar errores.

Cronología de la divulgación privada y pública

La vulnerabilidad se divulgó de forma privada a los responsables de los módulos afectados el 23 de abril de 2018. Unos días antes de que venciera el plazo de 90 días para la divulgación pública, se ofreció a los responsables la posibilidad de aplazarla por cualquier motivo. Además, nos pusimos en contacto con los módulos dependientes más activos (cuando había datos de contacto disponibles en GitHub) y los preparamos para la divulgación pública del 25 de junio de 2018.

Gracias a Karen Yavine, Simon Maple y Danny Grander, de Snyk, por ayudar con la divulgación pública, sugerir esta publicación y realizar más investigaciones. Agradezco también especialmente a Matt Sergeant, de Haraka, por responder con prontitud.

haraka

(versiones < 2.8.19)

https://snyk.io/vuln/npm:haraka:20180625

April 23rd, 2018 - Initial private disclosure to package owner
April 24th, 2018 - Initial response from package owner
June 15th, 2018 - Vulnerability fixed but not yet published to npm
June 25th, 2018 - Public disclosure
June 27th, 2018 - Version 2.8.19 published with fix

mailparser

(TODAS las versiones)

https://snyk.io/vuln/npm:mailparser:20180625

April 23rd, 2018 - Initial private disclosure to package owner
April 24th, 2018 - Initial response from package owner
June 25th, 2018 - Public disclosure
There is as yet no fix for mailparser.

emailjs-mime-parser

(TODAS las versiones)

https://snyk.io/vuln/npm:emailjs-mime-parser:20180625

April 23rd, 2018 - Initial private disclosure to package owner
April 24th, 2018 - Initial response from package owner
June 25th, 2018 - Public disclosure
There is as yet no fix for emailjs-mime-parser.

mailsplit

(versiones < 4.2.1)

https://snyk.io/vuln/npm:mailsplit:20180625

April 23rd, 2018 - Initial private disclosure to package owner
April 24th, 2018 - Initial response from package owner
June 25th, 2018 - Public disclosure
July 23rd, 2018 - Version 4.2.1 published with fix

mailparser-mit

(TODAS las versiones)

https://snyk.io/vuln/npm:mailparser-mit:20180625

April 23rd, 2018 - Initial private disclosure to package owner
April 24th, 2018 - Initial response from package owner
June 25th, 2018 - Public disclosure
There is as yet no fix for mailparser-mit.

Empieza con Capture the Flag

Aprende a resolver desafíos de Capture the Flag viendo nuestro taller virtual introductorio a pedido.