¿Por qué las expresiones regulares malvadas son un problema?
Porque las computadoras hacen exactamente lo que les dices que hagan, incluso si no es lo que querías decir o es totalmente irrazonable. Si le pide a un motor Regex que demuestre que, para una entrada determinada, existe o no una coincidencia para un patrón determinado, entonces el motor intentará hacerlo sin importar cuántas combinaciones diferentes deban probarse.
Aquí hay un patrón simple inspirado en el primer ejemplo en la publicación del OP:
^((ab)*)+$
Dada la entrada:
abababababababababababab
El motor de expresiones regulares intenta algo como (abababababababababababab)
y se encuentra una coincidencia en el primer intento.
Pero luego tiramos la llave inglesa:
abababababababababababab a
El motor lo intentará primero, (abababababababababababab)
pero eso falla debido a ese extra a
. Esto provoca un seguimiento catastrófico, porque nuestro patrón (ab)*
, en una muestra de buena fe, liberará una de sus capturas ("retrocederá") y dejará que el patrón externo vuelva a intentarlo. Para nuestro motor de expresiones regulares, se parece a esto:
(abababababababababababab)
- No
(ababababababababababab)(ab)
- No
(abababababababababab)(abab)
- No
(abababababababababab)(ab)(ab)
- No
(ababababababababab)(ababab)
- No
(ababababababababab)(abab)(ab)
- No
(ababababababababab)(ab)(abab)
- No
(ababababababababab)(ab)(ab)(ab)
- No
(abababababababab)(abababab)
- No
(abababababababab)(ababab)(ab)
- No
(abababababababab)(abab)(abab)
- No
(abababababababab)(abab)(ab)(ab)
- No
(abababababababab)(ab)(ababab)
- No
(abababababababab)(ab)(abab)(ab)
- No
(abababababababab)(ab)(ab)(abab)
- No
(abababababababab)(ab)(ab)(ab)(ab)
- No
(ababababababab)(ababababab)
- No
(ababababababab)(abababab)(ab)
- No
(ababababababab)(ababab)(abab)
- No
(ababababababab)(ababab)(ab)(ab)
- No
(ababababababab)(abab)(abab)(ab)
- No
(ababababababab)(abab)(ab)(abab)
- No
(ababababababab)(abab)(ab)(ab)(ab)
- No
(ababababababab)(ab)(abababab)
- No
(ababababababab)(ab)(ababab)(ab)
- No - No - No
(ababababababab)(ab)(abab)(abab)
- No
(ababababababab)(ab)(abab)(ab)(ab)
- No
(ababababababab)(ab)(ab)(ababab)
- No
(ababababababab)(ab)(ab)(abab)(ab)
- No
(ababababababab)(ab)(ab)(ab)(abab)
- No
(ababababababab)(ab)(ab)(ab)(ab)(ab)
- No
...
(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(abababab)
- No
(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ababab)(ab)
- No
(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(abab)(abab)
- No
(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(abab)(ab)(ab)
- No
(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ababab)
- No
(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(abab)(ab)
- No
(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(abab)
- No
(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)(ab)
- No
El número de combinaciones posibles escala exponencialmente con la longitud de la entrada y, antes de que te des cuenta, el motor de expresiones regulares está consumiendo todos los recursos de tu sistema tratando de resolver esto hasta que, habiendo agotado todas las combinaciones posibles de términos, finalmente se da por vencido y informa "No hay coincidencia". Mientras tanto, su servidor se ha convertido en una pila ardiente de metal fundido.
Cómo detectar expresiones regulares malvadas
De hecho, es muy complicado. Yo mismo he escrito un par, aunque sé cuáles son y, en general, cómo evitarlos. Vea Regex tomando un tiempo sorprendentemente largo . Envolver todo lo que pueda en un grupo atómico puede ayudar a prevenir el problema del retroceso. Básicamente, le dice al motor de expresiones regulares que no vuelva a visitar una expresión determinada: "bloquee lo que haya coincidido en el primer intento". Sin embargo, tenga en cuenta que las expresiones atómicas no evitan el retroceso dentro de la expresión, por ^(?>((ab)*)+)$
lo que sigue siendo peligroso, pero ^(?>(ab)*)+$
seguro (coincidirá (abababababababababababab)
y luego se negará a ceder cualquiera de sus caracteres coincidentes, evitando así un retroceso catastrófico).
Desafortunadamente, una vez que está escrito, en realidad es muy difícil encontrar inmediatamente o rápidamente una expresión regular problemática. Al final, reconocer una expresión regular incorrecta es como reconocer cualquier otro código incorrecto : se necesita mucho tiempo y experiencia y / o un solo evento catastrófico.
Curiosamente, desde que se escribió esta respuesta por primera vez, un equipo de la Universidad de Texas en Austin publicó un artículo que describe el desarrollo de una herramienta capaz de realizar análisis estáticos de expresiones regulares con el propósito expreso de encontrar estos patrones "malvados". La herramienta fue desarrollada para analizar programas Java, pero sospecho que en los próximos años veremos más herramientas desarrolladas para analizar y detectar patrones problemáticos en JavaScript y otros lenguajes, especialmente a medida que la tasa de ataques ReDoS continúa aumentando .
Detección estática de vulnerabilidades DoS en programas que utilizan expresiones regulares
Valentin Wüstholz, Oswaldo Olivo, Marijn JH Heule e Isil Dillig
La Universidad de Texas en Austin