Entender una regex (y no tumbar tu servidor con un ReDoS)
Las expresiones regulares son de esas cosas que escribes una vez, copias mil veces y no vuelves a mirar. El problema: una regex que funciona puede a la vez ser ilegible y, peor, un vector de denegación de servicio. Vamos con las dos cosas: leerlas y no dispararte en el pie.
Una regex se lee token a token
No intentes leerla de un vistazo; descomponla. Cada pieza es una de estas:
- Clases abreviadas:
\d(dígito),\w(palabra: A-Z, a-z, 0-9, _),\s(espacio). En mayúscula, la negación:\D,\W,\S. - Clases:
[a-z](uno de),[^0-9](cualquiera menos). - Cuantificadores:
*(cero o más),+(una o más),?(opcional),{2,5}(entre 2 y 5). Con?detrás, son perezosos (lo mínimo). - Anclas:
^(inicio),$(fin),\b(límite de palabra). - Grupos:
(...)captura,(?:...)agrupa sin capturar,(?=...)lookahead,(?<name>...)con nombre.
Ejemplo: ^(?<user>[a-z0-9._%+-]+)@([a-z0-9.-]+)\.[a-z]{2,}$ es «inicio, un grupo con nombre user de una o más letras/dígitos/símbolos, una arroba, un dominio, un punto y un TLD de dos o más letras, fin». Leída así, deja de dar miedo.
La trampa: catastrophic backtracking (ReDoS)
El motor de regex, cuando algo puede casar de muchas formas, prueba combinaciones y retrocede (backtracking) cuando falla. Con ciertos patrones, el número de combinaciones crece exponencialmente con la longitud de la entrada. Resultado: una cadena de 30 caracteres puede colgar la CPU durante segundos o minutos. Eso es un ReDoS (Regular expression Denial of Service).
La firma clásica es un cuantificador anidado sobre algo que se solapa:
(a+)+$ # grupo con + , cuantificado con + otra vez
(.*)* # lo mismo con .*
(\d+)* # y con \d+
(a|a)* # alternativa que se solapa, repetida
Con la entrada adecuada (muchas a seguidas de un carácter que rompe la coincidencia), el motor explora todas las particiones posibles. Un caso real famoso fue una regex de validación de correo «mejorada» que tumbaba servicios enteros.
Cómo evitarlo
- No anides cuantificadores sobre grupos que ya repiten. Reescribe
(a+)+comoa+. - Sé específico: en vez de
.*, usa una clase acotada como[^"]*o[^\n]{0,200}. - Ancla la expresión (
^…$) para que el motor no pruebe mil puntos de inicio. - Limita la entrada antes de aplicar la regex (longitud máxima razonable).
- En lenguajes con grupos atómicos o cuantificadores posesivos (
(?>…),a++), úsalos. Ojo: JavaScript no los tiene; en JS toca reescribir el patrón o cambiar de estrategia (p. ej. parsear a mano).
Encoding no es cifrado (y una regex no es un validador semántico)
Dos avisos que van juntos: una regex valida forma, no intención. Casar el formato de un email no garantiza que exista, ni que un input «limpio» sea seguro para SQL o HTML (para eso, consultas parametrizadas y escapado). La regex es una primera criba, no la defensa.
Checklist
- ✅ Léela token a token; si no la entiendes, no la mantengas.
- ✅ Busca cuantificadores anidados (
(x+)+): es la firma del ReDoS. - ✅ Prefiere clases acotadas a
.*; ancla con^…$. - ✅ Limita la longitud de la entrada antes de aplicar la regex.
- ✅ Recuerda: en JS no hay grupos atómicos; reescribe si hace falta.
Una buena regex es la que entiendes de un vistazo y no puede tumbarte. Si no cumple las dos, reescríbela.