Seguridad grave: cómo detener los encabezados HTTP dudosos que obstruyen su sitio web

Está en una larga cola en la estación y su tren llegará pronto, pero hay cuatro ventanillas de boletos abiertas y las cosas se están moviendo rápida y suavemente.

Tendrá tiempo de sobra para comprar su boleto, ir a la plataforma y salir de viaje.

Pero luego uno de los oficiales de boletos pone un cartel de POSICION CERRADA y sale del turno; TI llega al servicio de la máquina de tarjetas de crédito en la segunda ventana; la tercera ventana tiene un atasco de papel …

… y oye al cliente en la última ventana de trabajo decir: "He cambiado de opinión, no quiero ir a través de Leeds después de todo, así que me gustaría cancelar estas entradas que acabo de comprar y encontrar una ruta más barata ".

Una demora que hubiera sido poco más que una irritación en cualquier otro momento termina causando un ataque de denegación de servicio en su viaje.

No le tomará una hora extra comprar su boleto, pero le llevará una hora más esperar el próximo tren después de que haya perdido por poco el tiempo que usted pensó que había programado perfectamente.

Si esto le ha sucedido alguna vez, apreciará este documento de investigación de la reciente conferencia USENIX 2018: Congelando la Web: un estudio de vulnerabilidades ReDoS en servidores web basados ​​en JavaScript.

La conexión entre los trenes perdidos y los sitios web congelados puede no ser obvia de inmediato, así que vamos a explicarlo.

Los servidores web de Old School (OK, old-school-ish) como Apache generalmente usan un proceso o subproceso independiente para manejar cada conexión que llega.

Los procesos y los subprocesos no son lo mismo; por lo general, un proceso es una aplicación en ejecución por derecho propio; cada proceso puede subdividirse en varias sub-aplicaciones llamadas subprocesos, a veces llamadas procesos ligeros . Sin embargo, puede pensar en procesos y subprocesos como programas: son creados, lanzados, programados, administrados y eliminados por el sistema operativo. Esto significa que hay una gran cantidad de gastos generales para iniciarlos, cambiarlos y detenerlos, una carga que realmente puede ser un obstáculo en un servidor web ocupado que maneja cientos o miles de conexiones efímeras cada segundo.

En la analogía de la taquilla de nuestra estación, un servidor web multiproceso o de subprocesos transfiere su propia ventana de ticket temporal cuando llega, y se le asigna independientemente de lo que esté sucediendo en otras ventanas de boletos.

Bajo una carga modesta, esto funciona muy bien, porque está aislado de los efectos de mezclarse con otros viajeros, algunos de los cuales pueden hacer preguntas que inesperadamente tardan mucho tiempo en responder y detener a todos.

Pero bajo una gran carga, el modelo de multiproceso rápidamente se atasca: el servidor y el sistema operativo tardan más en crear ventanas de tickets separadas para todos que en emitir boletos, de modo que todos esperan más de lo necesario.

Reacciona al tráfico, no a las conexiones

Muchos servidores web modernos, por lo tanto, notablemente aquellos escritos en JavaScript usando entornos de programación como Node.js , son impulsados ​​por eventos en lugar de por conexión .

En términos generales, el sistema asigna un proceso para manejar todo el tráfico de la red, trabajando en las transacciones pendientes de cualquier usuario solo cuando hay algo nuevo que hacer.

Este modelo es más parecido al área de espera en una hamburguesería o en una tienda de pescado y papas fritas, donde realiza su pedido a su llegada; cuando está listo, alguien llama a su número para que pueda recoger su comida y salir.

Mientras espera, no tiene sentido que un miembro del personal asignado especialmente lo cuide, o una caja registradora ya asignada a usted, pero amarrada hasta que llegue el momento de pagar.

El personal de un bar de hamburguesas o de una tienda de chips podría ignorarte mientras esperas (después de todo estarás esperando) y seguir sin distraerse con otro trabajo.

Siempre que lo avisen lo suficientemente rápido cuando su pedido esté listo, la eficiencia general será generalmente más alta y, por lo tanto, el tiempo total de espera de todos será más corto.

Cuando las cosas se cierran

El modelo impulsado por eventos tiene un punto débil: puede obstruir desastrosamente si incluso unos pocos pasos clave en el proceso toman más tiempo del previsto.

Eso es un poco como ver a tu hamburguesa congelarse lenta y tristemente detrás del mostrador si eres el número 32, pero la persona que llama a los pedidos es rastreada tratando de resolver una discusión con el cliente 31.

En otras palabras, los servidores basados ​​en eventos pueden ser altamente eficientes …

… siempre que no se estanquen frente a eventos inusuales o inesperados.

Y eso es lo que nuestros investigadores buscaron: código de rutina de manejo de contenido en servidores web JavaScript que manejaba datos inesperadamente mal.

Deconstruyendo solicitudes web

Como se puede imaginar, los servidores web deben hacer una gran cantidad de coincidencias de patrones basadas en texto para deconstruir las solicitudes web entrantes, que podrían verse así:

 GET /products/info.html HTTP / 1.1
   Anfitrión: example.com
   User-Agent: Mozilla / 5.0
   Accept-Language: en; q = 0.5

El término de la jerga para separar la entrada de texto en sus componentes importantes es el análisis sintáctico .

En la solicitud web anterior:

  • La solicitud debe dividirse en líneas en cada marcador de final de línea o retorno de carro.
  • La línea GET debe dividirse en cada carácter de espacio.
  • Las líneas de encabezado deben separarse a cada lado del colon.
  • El encabezado Accept-language necesita dividirse en el punto y coma.
  • La configuración q=0.5 debe dividir en la forma "clave = valor" en el signo igual. (Q es una abreviatura de "calidad", aunque aquí significa "preferencia" sin sentido).

En muchos lenguajes de programación modernos, especialmente JavaScript, la técnica de ir para la coincidencia de texto es una herramienta llamada Expresión regular , o RE para abreviar.

Ahí es donde proviene el nombre de la amenaza que suena genial ReDoS en el título del papel: significa la denegación de servicio (empantanamiento del servidor) a través de expresiones regulares .

La coincidencia de texto es simple

Veamos el problema de tomar la cadena de texto q=0.5 y dividirla en q y 0.5 , para que podamos interpretar su significado.

La forma de hacerlo de la vieja escuela, familiar para cualquiera que haya usado BASIC, sería codificarla a mano, comenzando con algo como esto:

 POS = ENCONTRAR (STR $, '=')

Asumimos que FIND busca la posición del primer signo de carácter '=' en la variable de texto STR $ y devuelve su posición, de 1 (el desplazamiento del primer caracter) a LEN (STR $), la longitud de la cuerda (el desplazamiento del último carácter).

Si FIND vuelve con cero, significa que no encontró nada; de lo contrario, nos dice si la coincidencia ocurrió, por lo que sabemos dónde dividir la entrada en la clave (el lado izquierdo) y el valor (el lado derecho).

Necesitamos tomar caracteres (POS-1) de la izquierda del signo igual para la clave; para el valor, tomamos caracteres POS menos que la longitud de la cadena desde el extremo derecho:

 POS = ENCONTRAR (STR $, "="): REM Busque un igual
SI POS> 0 ENTONCES: REM Si hay uno ...
   KEY $ = LEFT $ (STR $, POS-1): REM ... picar desde la izquierda
   VAL $ = DERECHA $ (STR $, LEN (STR $) - POS): REM ... y hacia la derecha
FIN

Ese código es bastante fácil de entender y muy eficiente, porque codifica con precisión el algoritmo simple de "buscar el punto de división y luego cortar".

Pero es un poco molesto tener que explicar el proceso de coincidencia del patrón en lugar de solo poder expresar un patrón y dejar que una utilidad coincida haga el trabajo, que es lo que puede hacer con expresiones regulares.

Aquí hay un RE que podría usarse, no de forma correcta pero efectiva, en lugar del código de conexión por cable anterior: (.+)=(.+) .

Los RE se ven crípticos, debido a la forma extraña y confusa en que usan caracteres como abreviaturas; de hecho, su aspecto críptico los hace fáciles de equivocarse:

Es fácil equivocarse

Una vez que te acostumbras a RE, sin embargo, es difícil destecharte, porque son simples y, bueno, expresivos.

En un lenguaje de programación contemporáneo, es posible que pueda reemplazar algo como esto, donde srt.find coincide con un carácter y str.sub extrae explícitamente una subcadena …

 pos = str.find ('=')
   si pos entonces
       key = str.sub (1, pos-1)
       val = str.sub (pos + 1)
   más
       clave = nada
   fin

… con el uso bastante más elegante, y más fácil de ajustar, de una biblioteca de correspondencia RE:

 clave, val = str.match ('(. +) = (. +)')

El problema, como señalan los investigadores del documento ReDoS, es que las RE como la de arriba hacen el trabajo que desean y pasan fácilmente las pruebas de aceptación de software, pero hacen su trabajo incorrectamente.

El problema es que el RE de arriba coincide en cualquier parte del texto de entrada, y la función str.match intenta tanto como puede para encontrar una coincidencia, un efecto secundario que no notará hasta que:

  • La entrada es más larga que unos pocos miles de caracteres.
  • La entrada NO contiene ningún signo de igual.

Tenga en cuenta que el único carácter al que este RE tiene que pegarse explícitamente es un signo igual.

Si no hay un signo igual, el RE intentará hacer coincidir su patrón comenzando en el desplazamiento 1 en la cadena de entrada, pero fallará; el emparejador intentará nuevamente desde el desplazamiento 2, y volverá a fallar; luego desde el tercer personaje a lo largo; y así.

Como resultado, probará su patrón contra los caracteres 1 a LEN en la entrada; luego redundantemente contra los personajes 2 a LEN, que fallarán nuevamente; y así sucesivamente hasta que quedan menos de tres caracteres. (El RE no puede coincidir con menos de tres caracteres: el patrón requiere al menos uno antes de los iguales, el mismo y al menos uno después).

Si hay caracteres LEN en la entrada, la función de coincidencia termina ejecutando tiempos LEN, frente a un promedio de LEN / 2 caracteres, que es proporcional al cuadrado de LEN, lo que provoca un aumento masivo en el rendimiento a medida que LEN aumenta.

Necesita decirle al repetidor de RE que bloquee la coincidencia con el inicio de la entrada para que solo atraviese la entrada una vez.

Para eso, puede usar los caracteres RE especiales ^ , lo que significa "emparejar solo desde el principio", y $ , lo que significa "emparejar solo el final".

La diferencia en el rendimiento es enorme y empeora cuadráticamente a medida que la entrada se hace más larga.

Tenga en cuenta que en los gráficos a continuación, el eje que muestra el rendimiento lineal de la coincidencia anclado encabeza a 2 / 1000ths de segundo para una cadena de entrada de 100K, mientras que la coincidencia sin anclaje de rendimiento cuadrácticamente toma más de un segundo completo para el mismo trabajo.

Eso es 500 veces más lento debido al carácter de bloqueo faltante ^ al comienzo de la RE, con la discrepancia empeorando a un ritmo cada vez mayor a medida que aumenta el tamaño de la entrada:

¿Qué hacer?

El documento ReDos no es una noticia terriblemente mala, pero es un potente recordatorio de que el código que se ve bien, y que realmente funciona, puede fallar mal en la vida real.

En particular, obstruir un servidor web basado en eventos con encabezados HTTP patológicamente diseñados (el análisis que se realiza de forma rutinaria para cada solicitud entrante) podría hacer que un servidor de otra manera seguro sea fácil de "congelar" con un ataque DoS.

Los investigadores descubrieron una serie de servidores del mundo real que, en teoría, podrían ponerse de rodillas fácilmente debido al uso excesivamente casual de expresiones regulares para preprocesar encabezados web.

En algunos casos, los riesgosos RE, las posibles causas de un ReDos, no eran exclusivos del código de servidor personalizado de una empresa, sino que se heredaban de una biblioteca de programación estándar.

Afortunadamente, los problemas se solucionan fácilmente:

  • Revise el código de coincidencia de patrón cuidadosamente. El rendimiento es importante, especialmente cuando puede verse afectado por datos no confiables enviados desde el exterior.
  • Incluya muestras patológicas en su conjunto de prueba. No pruebe la coincidencia de patrones solo para sus capacidades de aceptación / rechazo: pruebe que puede aceptar o rechazar entradas dentro de límites de tiempo bien definidos.
  • Limite cuánto tiempo puede ejecutarse cualquier función de coincidencia de patrones. Los investigadores encontraron numerosos servidores web con RE que ejecutados en un tiempo que fue exponencial con la longitud de la entrada, es mucho más dramático y arriesgado que el rendimiento cuadrático que mostramos anteriormente.
  • Trate los patrones que funcionan mal como un signo de problemas. Los RE complejos que ocasionalmente se presentan durante años tienen más probabilidades de coincidir donde no deberían, lo que se conoce como un falso positivo. Cuanto más tiempo se ejecuta una RE, más difícil es tratar de encontrar una coincidencia y más tiempo tiene para encontrar una imprevista.
  • Lea el documento ReDos. Algunos de los RE problemáticos que se encuentran en el uso del mundo real provienen de bibliotecas estándar, por lo que no es solo su propio código el que está en riesgo: aprenda cómo detectar problemas antes de que sucedan.
  • Divide y conquistaras. Una serie de comprobaciones sencillas y fáciles de entender es mucho más fácil de probar y mantener que una supercombinación megacomplejos que intente empaquetar múltiples pruebas en una sola fórmula arcana.