Backtracking Catastrófico y Regex Vulnerables a ReDoS
Dev Tools

Backtracking Catastrófico y Regex Vulnerables a ReDoS

El Patrón Inocente Que Congela Tu Servidor

Una expresión regular suele ser una de las operaciones más baratas de tu código. Sin embargo, un único patrón escrito sin cuidado puede tardar segundos, minutos o prácticamente una eternidad en evaluar una cadena corta — y un atacante que lo descubra puede bloquear todo un hilo de petición con una sola llamada HTTP. Este fallo se llama backtracking catastrófico, y cuando es explotable se convierte en una Denegación de Servicio por Expresiones Regulares (ReDoS). Este artículo explica el mecanismo con precisión, te enseña a reconocer patrones peligrosos y te da reescrituras concretas que puedes validar en nuestro Probador de Regex.

Si has leído nuestra guía completa de sintaxis de regex, ya sabes que los cuantificadores son codiciosos por defecto y que el motor retrocede cuando una coincidencia falla. ReDoS es lo que ocurre cuando ese retroceso explota de forma combinatoria.

Cómo Funciona Realmente el Backtracking

La mayoría de los motores de regex populares — incluido el de JavaScript (ECMAScript), java.util.regex de Java, el módulo re de Python y PCRE — usan un algoritmo de retroceso en lugar de un autómata finito. Cuando un cuantificador codicioso consume demasiado y el resto del patrón no puede coincidir, el motor devuelve caracteres uno a uno y reintenta. Para un patrón simple esto es barato. El peligro aparece cuando el motor tiene muchas maneras distintas de repartir la misma entrada entre dos cuantificadores solapados, porque puede probarlas todas antes de concluir que no hay coincidencia.

El detonante clásico de manual es un cuantificador aplicado a un grupo que a su vez contiene un cuantificador, donde ambos pueden coincidir con los mismos caracteres. Considera (a+)+$ probado contra una cadena de muchas a seguidas de un solo !. El + externo e interno pueden particionar la sucesión de a de un número exponencial de formas. Cada partición es un camino distinto que el motor debe explorar antes de que el $ final falle ante el !. El trabajo crece del orden de 2 elevado a la longitud de la entrada — veinticinco caracteres ya pueden significar decenas de millones de pasos.

Las Tres Formas del Peligro

El backtracking catastrófico casi siempre viene de una de tres estructuras reconocibles. Aprender a verlas de un vistazo es la habilidad más valiosa aquí.

Cuantificadores anidados

Un cuantificador envolviendo un grupo que contiene otro cuantificador sobre un conjunto de caracteres solapado: (a+)+, (\d+)*, (a*)*, ([a-z]+)+. Como la repetición interna y externa pueden consumir los mismos caracteres, el número de formas de dividir la entrada es exponencial.

Alternancia cuantificada con solapamiento

Una alternancia dentro de un cuantificador cuyas ramas pueden coincidir con el mismo texto: (a|a)*, (\w|\d)*, (.|\s)*. Aquí \w ya incluye \d, así que por cada dígito el motor tiene dos opciones igual de válidas, duplicando el espacio de búsqueda en cada paso.

Cuantificadores adyacentes compitiendo por los mismos caracteres

Dos cuantificadores en secuencia sobre la misma clase, unidos por un ancla o literal que puede fallar: \d+\d+$ o el más sutil .*.*=.*. Cuando la restricción final falla, el motor redistribuye caracteres entre las dos partes codiciosas en combinaciones cuadrática o exponencialmente numerosas.

Un Ejemplo Práctico Que Puedes Sentir

Abre el Probador de Regex y prueba ^(\w+\s?)*$ — un patrón que parece un validador perfectamente razonable de "palabras separadas por espacios opcionales". Contra una entrada limpia como hello world coincide al instante. Ahora dale una cadena de treinta caracteres de palabra seguida de un solo espacio y un signo de exclamación, por ejemplo aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa !. El ! final no puede ser parte de una palabra ni es un espacio, así que $ falla — y el motor recorre un número astronómico de formas de repartir esos treinta caracteres entre el \w+ interno y el * externo. El patrón que corría en microsegundos ahora se cuelga.

Por esto exactamente el proyecto OWASP mantiene una página dedicada a la Denegación de Servicio por Expresiones Regulares, y por lo que la Common Weakness Enumeration lo cataloga como CWE-1333 — Complejidad Ineficiente de Expresiones Regulares. La causa raíz nunca es el tamaño de la entrada — es la estructura del patrón.

Por Qué Es un Problema de Seguridad, No Solo de Rendimiento

En un bucle de eventos de un solo hilo como Node.js, la evaluación de una expresión regular es síncrona y bloquea todo. Mientras una petición está atascada en backtracking catastrófico, ninguna otra petición de ese proceso puede atenderse — las comprobaciones de salud fallan, el balanceador de carga puede marcar la instancia como muerta, y una única cadena fabricada se convierte en una denegación de servicio completa. Esto no es teórico. La caída de Cloudflare del 2 de julio de 2019 la causó una expresión regular en su WAF que contenía .*.*=.*, que retrocedió catastróficamente y llevó la CPU global al 100 por cien. La caída de Stack Overflow del 20 de julio de 2016 se rastreó hasta un patrón que coincidía con espacios en blanco finales de una línea larga.

Como la entrada del usuario fluye con frecuencia hacia las regex — validar correos, analizar cabeceras User-Agent, sanear marcado, comprobar valores JSON extraídos en nuestro flujo del Formateador JSON — cualquier patrón que toque datos no confiables debe asumirse alcanzable por un atacante.

Backtracking Catastrófico y Regex Vulnerables a ReDoS

Cómo Detectar Patrones Vulnerables

La detección funciona en tres niveles, y deberías usar los tres.

  • Lee buscando las tres formas. Examina cada patrón en busca de cuantificadores anidados, alternancia cuantificada solapada y cuantificadores adyacentes de la misma clase. Si ves (X+)+, (X*)* o (X|Y)* donde X e Y se solapan, trátalo como culpable hasta que se demuestre lo contrario.
  • Prueba con una cadena adversaria. Construye el peor caso deliberadamente — una sucesión larga del carácter que el cuantificador interno acepta, terminada en un carácter que fuerce al ancla o literal final a fallar. Pégala en el Probador de Regex y observa si la coincidencia sigue siendo instantánea. Un probador moderno que avisa ante tiempos largos de evaluación convierte esto en una comprobación de un segundo.
  • Ejecuta un analizador estático. Herramientas como la regla ESLint eslint-plugin-redos, el motor de código abierto recheck y la heurística safe-regex marcan estructuras peligrosas en tiempo de linting. Son imperfectas — las heurísticas producen falsos negativos — pero detectan los casos obvios automáticamente en toda una base de código.

Cómo Corregirlo

No hay una solución única; hay una caja de herramientas. Elige según lo que el patrón intenta expresar realmente.

Hacer imposible el solapamiento

La corrección más limpia elimina la ambigüedad que permite elegir al motor. El vulnerable ^(\w+\s?)*$ existe para coincidir con palabras separadas por espacios, así que exprésalo directamente con piezas no solapadas: ^\w+(?:\s+\w+)*$. Ahora cada carácter pertenece a exactamente una parte del patrón — no hay forma de redistribuirlo, así que el backtracking no puede explotar. Esta reescritura de camino único es la técnica más fiable y debería ser tu opción por defecto.

Anclar la repetición interna

Reemplaza una clase permisiva por una más específica para que el cuantificador interno no pueda consumir el delimitador. Si coincides con cadenas entrecomilladas, no escribas ".*" — escribe "[^"]*", que detiene la coincidencia interna en la comilla de cierre y elimina toda ambigüedad sobre dónde termina la cadena.

Usar grupos atómicos o cuantificadores posesivos

Los motores que los admiten — PCRE, Java, Ruby y .NET — ofrecen grupos atómicos (?>…) y cuantificadores posesivos a++, a*+. Estos le dicen al motor que se comprometa con una coincidencia y nunca devuelva caracteres, lo que elimina por completo el backtracking. JavaScript históricamente carecía de ellos, pero los modificadores de expresiones regulares de TC39 y la adopción más amplia de estas construcciones hacen que debas recurrir a ellos cuando tu motor lo permita. Donde no estén disponibles, simula la atomicidad con el modismo de lookahead más referencia hacia atrás: (?=(a+))\1.

Acotar la entrada y la repetición

La defensa en profundidad ayuda incluso tras una corrección estructural. Rechaza entradas demasiado largas antes de que lleguen a la regex, y prefiere cuantificadores acotados como {1,64} frente a un + abierto donde el dominio lo permita. Un límite de longitud convierte una posible explosión exponencial en una constante meramente grande.

Cambiar a un motor de tiempo lineal

La respuesta más duradera es dejar de usar un motor con backtracking para entrada no confiable. Los motores construidos sobre simulación de NFA de Thompson — RE2 de Google, el crate regex de Rust y el paquete regexp estándar de Go — garantizan una evaluación en tiempo lineal respecto a la longitud de la entrada, sin backtracking y por tanto sin caso catastrófico. Omiten deliberadamente las referencias hacia atrás y el lookaround, las mismas funciones que fuerzan el backtracking, un intercambio que la mayoría del código de validación puede aceptar con gusto.

Una Lista de Verificación Antes de Publicar un Patrón

  • ¿Algún cuantificador envuelve un grupo que contiene otro cuantificador sobre una clase solapada? Reescríbelo a un camino único.
  • ¿Una alternancia dentro de un cuantificador tiene ramas que coinciden con los mismos caracteres? Colápsalas o sepáralas.
  • ¿Se ejecutará este patrón alguna vez sobre entrada no confiable? Si es así, limita la longitud de la entrada y considera RE2 o el crate regex de Rust.
  • ¿Has probado el peor caso — una sucesión larga que acepta terminada en un carácter que rechaza — y confirmado que sigue siendo rápido?
  • ¿Hay un analizador estático como recheck o eslint-plugin-redos en tu pipeline de CI?

Llevándolo a la Práctica

El backtracking catastrófico es uno de esos errores invisibles en cada prueba que escribes y obvio para la primera persona que te ataca. La defensa no es memorizar una lista negra de patrones malos sino entender el mecanismo — la repetición solapada multiplica el espacio de búsqueda del motor — y adquirir el hábito de probar cada patrón con una cadena deliberadamente hostil. Toma cada regex de este artículo, pégala en el Probador de Regex junto a su entrada adversaria, y observa la diferencia entre la forma vulnerable y la reescrita. Esa respuesta directa e inmediata es lo que convierte una advertencia abstracta en un reflejo.

Para una base más profunda sobre la mecánica de cuantificadores y agrupación que subyace a todo esto, mantén abierta nuestra guía completa de sintaxis de regex en otra pestaña. Los dos artículos están diseñados para leerse juntos — uno te enseña a escribir patrones, este te enseña a escribir patrones que no puedan volverse en tu contra.

← Volver al Blog