El martes 2 de julio de 2019, un ingeniero del
equipo de Firewall de Cloudflare estaba trabajando en algunas mejoras menores para detectar
ataques de secuencias de comandos entre sitios. En este tipo de ataque, los actores maliciosos pueden
tomar un sitio web real y agregar código HTML o JavaScript a la URL de tal manera que se
ejecute automáticamente cuando se carga el sitio. Este código podría, por ejemplo, acceder a las
cookies y los tokens de sesión de un usuario y enviárselos al hacker. Envíe por correo electrónico esa URL maliciosa a una
víctima analfabeta informática desprevenida con 99 millones de dólares en su cuenta bancaria, y el efectivo
es tan bueno como el suyo. Como red de entrega de contenido, Cloudflare
tiene miles de servidores repartidos por todo el mundo en varios puntos de presencia o PoP, a
los que los usuarios cercanos envían sus solicitudes de sitios web.
Estos servidores perimetrales pueden validar las solicitudes
antes de reenviarlas al servidor de origen y, en el caso de Cloudflare, las comprobaciones de seguridad
se realizan mediante su software de firewall de aplicaciones web. Las reglas de firewall a menudo se configuran mediante
expresiones regulares, que son solo cadenas de símbolos donde cada símbolo representa una
instrucción de coincidencia particular. Resumen rápido de expresiones regulares, si quisiera detectar
el nombre Kevin Fang, usaría la expresión Kevin Fang. Ahora, si quisiera hacer coincidir a cualquier Kevin con
un apellido que comience con F, podría reemplazar todo lo que pasa después de F con un punto, que coincide con
cualquier carácter, y un asterisco, que modifica el símbolo anterior para que coincida con cualquier cantidad de
ese símbolo, en este caso, cualquier número de cualquier carácter.
Entonces, se daría cuenta de que Kevin F1234$%
no es un nombre real y, en su lugar, reemplazaría el punto con este, que solo coincide con los
caracteres alfabéticos. Entonces, ¿cómo se pueden usar las expresiones regulares en las reglas de firewall
para hacer coincidir las URL sospechosas? Digamos que desea hacer coincidir los comandos SQL, ya que las
URL legítimas generalmente no los contienen. ¿ Qué pasaría si quisiera hacer coincidir los ataques transversales de directorio
, donde un hacker puede leer archivos arbitrarios en su servidor web? Bueno, puedes hacer coincidir el punto doble.
Bueno, el ingeniero estaba terminando de escribir
una nueva regla de firewall, que debía detectar secuencias de comandos entre sitios. Vemos que está tratando de hacer coincidir algunas
palabras clave de Javascript, así como algunos caracteres que se pueden usar para salir del contexto html y javascript
, como estas comillas y corchetes de cierre. También intenta hacer coincidir esta cadena con
iguales, lo que potencialmente corresponde a la cadena de consulta de una URL, un punto de entrada común
para los ataques de secuencias de comandos entre sitios. Sin embargo, solo podemos especular, ya que Cloudflare
no explicó las intenciones detrás de esta regla. Pero probablemente solo estaban probando un montón
de cosas para ver qué se pegaba. Además, si realmente se estuviera gestando una salsa de cortafuegos secreta
aquí, podrían haber liberado fácilmente solo la parte ofensiva que causó
el desastre, en lugar de toda la regla. Correcto, entonces el ingeniero terminó de escribir
algunas de estas reglas, incluida esta rota, y envió una solicitud de extracción a su
repositorio de BitBucket.
Luego, como parte de la solicitud de extracción, la
plataforma de implementación e integración continua de TeamCity creó el paquete y ejecutó todas las pruebas, lo que les
dio a los revisores la confianza de que el código no era pura basura. Otros en el equipo echaron un vistazo, expresaron
que les parecía bien y aprobaron y fusionaron el cambio. Después de esto, TeamCity creó y
probó automáticamente el paquete una vez más y generó una solicitud de cambio. Después de que el gerente
o el líder técnico apruebe esta solicitud, el cambio se implementará automáticamente. El plan era lanzar esto en modo "simular"
donde el tráfico de clientes no está bloqueado, pero se recopilarán métricas para que los ingenieros
analicen la efectividad. Entonces, ¿cómo implementa Cloudflare las cosas? Normalmente, esto implica 4 etapas. En primer lugar, se implementa en un punto
de presencia DOG de prueba que solo utilizan los empleados internos para detectar problemas antes de que afecten al cliente.
Luego, va al PIG PoP utilizado por los
clientes que no pagan y que a nadie le importa, luego, va a algunos Canary Pops, cada uno de los cuales tiene
un pequeño subconjunto de todos los clientes y, por último, se implementa globalmente para todos los demás. Excepto que las implementaciones de WAF en realidad no siguen
este proceso en absoluto, se extiende inmediatamente a todo el mundo, debido a la necesidad de responder rápidamente a las
nuevas amenazas.
Genial, después de que se aprobó la solicitud de cambio,
comenzó la implementación global. Por lo general, esta sería una implementación continua,
donde los servidores se actualizan lote por lote, ya que los cambios de código generalmente requieren un
tiempo de inactividad para detener, actualizar y reiniciar el software, y probablemente no desee
hacerlo en todos los servidores al mismo tiempo. tiempo. Sin embargo, las reglas WAF tenían la opción de implementación única
de escribirse directamente en una base de datos compartida globalmente, conocida internamente como Quicksilver
o Velocireplicator, aunque no se lo digas a nadie. Esto no requiere ningún tipo de tiempo de inactividad, lo que
permite que Cloudflare implemente instantáneamente reglas WAF en todo el mundo, lo cual es útil siempre que
haya amenazas emergentes graves, por ejemplo, el log4j CVE de 2021. La regla rota se implementó mediante este procedimiento,
por lo que se propagó a todos Los servidores de Cloudflare en segundos. Tres minutos después, se llamó a los ingenieros
por fallas de WAF, lo que indicó que todos los puntos de presencia en todo el mundo estaban fallando
al mismo tiempo.
Mirando más allá, vieron que todos sus
servidores se ejecutaban en hiperimpulsor y estaban en rojo con un uso de CPU del 100%. Como resultado, estaban perdiendo hasta un 80 %
del tráfico de sus clientes. Ellos especularon que se debió a algún tipo de
ataque novedoso, pero no obstante, declararon un incidente P0 o sev0 y escalaron a todos. Millones de sitios web usan la CDN de Cloudflare,
por lo que una parte notable de Internet se cayó al mismo tiempo.
Piense en ello como si todos los centros de distribución de Amazon
cerraran repentinamente. A pesar de que los minoristas en línea y las
líneas de producción siguen funcionando, no tendrían forma de hacer llegar los productos
al consumidor. Correcto, entonces todo estaba en llamas, y los
ingenieros ahora estaban tratando de encontrar qué estaba causando el alto uso de la CPU. Había un montón de cosas ejecutándose en el
servidor, pero después de verificar el desglose de la CPU por proceso, quedó muy claro que el
proceso WAF era el responsable.
En este punto, 20 minutos después de la interrupción,
no era hora de perder el tiempo y pensar por qué sucedió esto, tuvieron que cerrar
el WAF de inmediato. Todo el equipo miró fijamente al CTO
mientras buscaban permiso para ejecutar la finalización global, que era un indicador de función
que desactivaría el WAF para todos los clientes. Se otorgó el permiso, pero llegar a la
terminación global tomó un poco, ya que algunos de los servicios internos de Cloudflare también dependían del
propio Cloudflare.
De hecho, comer su propia comida para perros de esta manera es una
buena práctica, ya que le permite a la empresa tener experiencia de primera mano con su propio
producto. Además, los ingenieros tampoco estaban
muy familiarizados con el sistema de terminación global , ya que rara vez se usaba. El último uso puede haber sido hace 2 años
en 2017 cuando tuvieron que cerrar varias funciones para filtrar datos de solicitudes de clientes de texto sin formato
. Finalmente, en la marca de 25 minutos, finalizaron globalmente
el WAF, lo que hizo que los niveles de CPU y tráfico volvieran a la normalidad.
En este punto, la interrupción casi había terminado,
excepto que no había WAF. Después de aproximadamente 40 minutos de validación manual exhaustiva
, revirtieron las reglas rotas y volvieron a habilitar el WAF a nivel mundial. Todo volvió a la normalidad, pero ¿cómo es que la
regla del Firewall hizo que los niveles de la CPU aumentaran? Así que el problema estaba en la última sección aquí. Tenemos un grupo que no captura, representado por
el signo de interrogación y los dos puntos, que especifica algo con lo que coincidir, pero que no debe incluirse
en la salida. Ahora que sabemos lo que hace un grupo que no captura
, podemos tirar todo eso por la ventana, ya que en realidad es irrelevante aquí. Ya sea que haya o no un grupo que no capture,
el motor Regex aún debe coincidir con la expresión contenida dentro, para que podamos ignorarlo con seguridad
.
Ya estamos familiarizados con todos estos
personajes. Al dividir la expresión en cuatro partes,
las tres estrellas de puntos coinciden con cualquier número de cualquier carácter, y el signo igual coincide
con un signo igual. Pero, ¿cómo es realmente el proceso de emparejamiento
? Intuitivamente, tiene sentido comenzar con el
primer símbolo, intentar hacer coincidir y luego pasar al siguiente, pero ¿cómo sabemos cuántos
caracteres hacer coincidir? Si hacemos coincidir todos los caracteres, se vuelve imposible
que el signo igual coincida con nada.
Y la respuesta es que no lo sabemos. La mayoría de los motores de expresiones regulares simplemente coinciden tanto como
pueden, retroceden en caso de falla y coinciden un poco menos la próxima vez, repitiendo esto hasta que
hay una coincidencia o se agotan todas las combinaciones. Para elaborar, cuando la expresión regular de Cloudflare intenta
hacer coincidir esta cadena de ejemplo, estos son los pasos que ocurren: Primero, esta primera estrella de punto coincide con avidez con
los tres caracteres A continuación, la segunda estrella de punto coincide con avidez con
los 0 caracteres restantes Luego, el signo igual falla, como los
emparejadores anteriores ya tomaron el signo igual. Después de esto, el algoritmo retrocede. La segunda estrella de puntos no coincidió con nada, por lo que no
hay nada que retroceder.
Sin embargo, la primera estrella de puntos coincidió con los tres
caracteres, así que retrocedemos y vemos qué sucede cuando hacemos coincidir dos. Luego, la segunda estrella de puntos ahora puede coincidir con la
x restante, pero el signo igual aún no coincide con nada. Así que ahora retrocedemos nuevamente, esta vez haciendo coincidir
0 caracteres con la segunda estrella de puntos, pero no funciona. Así que retrocedemos nuevamente al primer punto estrella,
haciendo coincidir solo un carácter. Ahora, la segunda estrella de puntos toma con avidez el
resto de los caracteres, y el signo igual se queda sin coincidencia, y tenemos que retroceder de
nuevo. Este ciclo continúa unas cuantas veces más hasta que
finalmente encuentra la coincidencia en 23 pasos. Si la cadena para hacer coincidir tiene una x más, el
número de pasos aumenta a 33. Luego a 45. Lo cual puede notar que no es lineal. De hecho, a medida que se hace más larga la cadena para hacer coincidir,
el aumento de pasos es exponencial, llegando fácilmente a los miles.
Si desea una expresión aún menos eficiente,
esta requiere más de un millón de pasos para que coincida con esta cadena de 20 caracteres. Entonces, haciendo algunos cálculos rápidos, cada servidor de Cloudflare recibe alrededor de 30 mil solicitudes por segundo, esta coincidencia de expresiones regulares de retroceso catastrófico se
realiza para un subconjunto de estas solicitudes y las URL de solicitud pueden ser bastante largas, lo que
hace que todos los servidores exploten. Entonces, ¿la expresión regular está rota? ¿ Deberíamos eliminarlo de todos nuestros
sistemas de producción? Bueno, no, estas expresiones simplemente estaban
mal escritas y podrían representarse de manera más eficiente. Sin embargo, para evitar que esto vuelva a suceder
, Cloudflare decidió cambiar a un algoritmo de expresión regular que siempre se ejecuta en tiempo lineal con respecto a la longitud de
la cadena, como el re2 de Google, que funciona convirtiendo la expresión regular en una máquina de estado, o fantasía
diagrama de flujo de la informática. Echemos un vistazo a cómo puedes
convertir ingenuamente esta expresión (a|b)* en un diagrama de flujo.
Esta línea vertical es un operador OR, por lo que
coincide con cualquier número de a o b. Tengamos un estado inicial y un estado final. Las transiciones entre estados pueden consumir un carácter
de la entrada, y si podemos alcanzar el estado final con una entrada completamente consumida, entonces
tenemos una coincidencia. Primero, sabemos que la estrella permite que esto coincida con
cadenas de longitud 0, por lo que podemos agregar el caso en el que la entrada es una cadena vacía e ir directamente
al estado final. Esta transición se denomina transición épsilon,
lo que significa que puede tomar esta ruta sin hacer coincidir ninguna parte de la cadena de entrada. Sin embargo, también podemos hacer coincidir las cadenas de 1 letra
"a" y "b", así que agreguemos un estado intermedio para cuando eso suceda, y una vez más
conéctelo al estado final con una transición épsilon, como después de consumir una "a".
o “b”, tenemos una coincidencia válida y podemos ir hasta el final.
Pero después de esta coincidencia, es posible que todavía tengamos más
caracteres en la cadena de entrada. Podemos agregar un bucle de regreso al estado inicial,
donde podemos hacer coincidir infinitamente más A y B. Y aquí lo tenemos: agregue aba, y
podemos ver fácilmente que puede llegar al final después de consumir toda la entrada. Agregue bc, y estamos atascados, incapaces de consumir
la c, lo que significa que no coincide. También podemos simplificar todo esto eliminando
todas las transiciones épsilon y haciendo que ambos estados transitorios sean estados finales válidos. Incluso podemos tener un solo estado que represente
tanto el inicio como el final, con una transición de varios caracteres apuntándose a sí mismo. Estos dos ejemplos simplificados son
autómatas finitos deterministas o DFA, es decir, a medida que consumimos cualquier cadena de entrada, solo habrá
una única ruta válida.
Y dado que solo hay una única
ruta válida, y la longitud de esta ruta es directamente 1: 1 con la cadena de entrada, ya que consumimos
un carácter por transición, se garantiza que este algoritmo de coincidencia se ejecutará 1: 1 o lineal
con el tamaño de la cadena de entrada. Ahora, el diagrama de flujo que teníamos antes era un
autómata finito no determinista o NFA, lo que significa que una entrada puede tener varias rutas. Como puede ver, podemos tomar la transición de la letra A o
épsilon aquí. Esto no es un gran problema aquí, ya que las
rutas épsilon terminan inmediatamente. Pero imagine si la expresión tuviera dos A,
reemplazando la B con una A. Ahora, si ingresamos una cadena que es todo A, nos bifurcamos repetidamente
dos veces ya que ambas rutas A son válidas, creando un número exponencial de rutas que se duplican
como nosotros vuelta atrás. Entonces, esta NFA claramente no garantiza una coincidencia de tiempo lineal, ya que este ejemplo requiere explorar un número exponencial de rutas.
Obviamente, podemos simplificar esto a algo
menos tonto, como la representación DFA. Y eso es exactamente lo que
hacen estos motores de expresiones regulares de tiempo lineal: convierte la expresión en un DFA, lo que garantiza el tiempo de ejecución lineal. Esto podría hacerse directamente, pero lo que
hace re2 es construir un NFA y atravesarlo con optimizaciones de almacenamiento en caché sobre la marcha que se aproximan
al DFA. En comparación con el retroceso tradicional, esto
requiere construir la máquina de estado, además de la memoria adicional necesaria para almacenarla. Entonces, en la práctica, re2 no es necesariamente
superior objetivamente en todos los sentidos, como podemos ver en estos problemas abiertos de Github. Bien, volvamos a Cloudflare. Un montón de cosas salieron mal aquí, pero la
mayor señal de alerta que no se mencionó para mí es que el plan de reversión requería ejecutar la
compilación WAF dos veces y tomaría demasiado tiempo. Esto tiene sentido, ya que la sección de reversión
en CR dijo que simplemente desharían el compromiso, fusionarían una nueva solicitud de extracción y
reconstruirían todo.
Lo cual es más un avance que
un retroceso. En teoría, debería haber sido posible
revertir el cambio en 2 segundos, tal como se implementó en 2 segundos. Si se hubiera guardado el estado anterior de Quicksilver
y se hubiera configurado para revertirse automáticamente cuando se activan las alarmas relevantes, esto
podría haber terminado como una interrupción de 3 minutos, ya que fue cuando se activaron las primeras alarmas.
Entonces, si bien todas estas iniciativas son buenas,
como agregar protecciones de uso de CPU, perfilar todas sus reglas y cambiar los motores de expresiones regulares,
parece que extrañamente falta hacer que las reversiones sean rápidas y automáticas . Por supuesto, igualmente importante aquí es
usar Quicksilver solo para cambios realmente emergentes y, de lo contrario, seguir el proceso de implementación estándar
. Que esto sea una advertencia para todos los que están realizando
coincidencias de expresiones regulares extremadamente complicadas a escala..