Uma expressão regular que funciona perfeitamente nos seus dados de teste pode, em uma entrada ligeiramente diferente, levar muito tempo para falhar. O padrão está certo; é a entrada que por acaso empurra o motor a explorar um número enorme de becos sem saída. Isso é backtracking catastrófico, e quando a entrada vem de um usuário, vira uma fraqueza de negação de serviço conhecida como ReDoS.
Por que o backtracking explode
Os motores de regex usados por JavaScript, Python, Java e a maioria dos outros são motores de backtracking. Quando uma correspondência falha, eles voltam e tentam outra forma de dividir a entrada entre os quantificadores. Normalmente isso são poucas tentativas. O problema começa quando dois quantificadores podem corresponder aos mesmos caracteres, dando ao motor muitas formas equivalentes de repartir o trabalho.
O exemplo clássico é (a+)+$. Tanto o + interno quanto o + externo querem a sequência de as, e há exponencialmente muitas formas de particionar uma string de as entre eles. Na entrada aaaaaaaaaaaaaaaaaaaaaaaa!, que não pode corresponder por causa do ! final, o motor tenta todas as partições antes de desistir — e cada a extra praticamente dobra o trabalho. Duas dúzias de caracteres já podem levar segundos; mais alguns e é praticamente uma eternidade. O mesmo formato se esconde em (.*)*, (\d+)* e (\w+\s?)+.
Como reconhecer o perigo
O sinal de alerta é um quantificador aplicado a um grupo que já contém um quantificador ilimitado sobre algo que o externo também poderia corresponder. (a+)+, (a*)* e (?:\d+)+ se encaixam. Alternância com sobreposição é a outra fonte clássica: (a|a)* ou (\w|\d)*, em que ambos os ramos podem corresponder ao mesmo caractere, multiplica os caminhos da mesma forma.
O Kit de Regex deste site faz uma verificação estática do formato de quantificador aninhado e avisa antes de deixar um padrão sinalizado rodar contra o seu texto, justamente para que uma única tecla não congele a página. A verificação é deliberadamente conservadora — prefere avisar sobre um padrão seguro a deixar passar um perigoso — então trate um aviso como "olhe com mais atenção", não como "isto está definitivamente quebrado".
Como escrever padrões que não podem explodir
Alguns hábitos removem quase todo o risco:
- Não aninhe quantificadores ilimitados. Se você se pegar escrevendo um
+ou*em um grupo que já contém um+ou*, reescreva. Muitas vezes o quantificador externo é simplesmente desnecessário. - Seja específico em vez de usar
.*. Corresponder a "tudo até uma aspa" é mais seguro como[^"]*do que como.*, porque uma classe negada não pode sobrepor o delimitador e, portanto, não pode voltar para dentro dele. - Ancore seu padrão. Um
^inicial (e um$final ao validar um campo inteiro) impede o motor de tentar a correspondência em cada posição da string. - Limite sua repetição.
\d{1,9}não pode disparar como\d+em entrada adversária; um limite superior explícito restringe a busca.
O JavaScript não oferece grupos atômicos nem quantificadores possessivos, os recursos que outros motores usam para proibir o backtracking de vez, então em JS a resposta é estrutural: escreva o padrão de forma que a ambiguidade nunca exista. Para entrada não confiável em escala, a opção mais robusta é um motor sem backtracking como o RE2, que garante tempo linear mas abre mão de retrorreferências e lookarounds.
O ReDoS é listado pela OWASP como uma classe de ataque real, não teórica — uma única requisição forjada pode prender uma CPU. Construa os padrões no Kit de Regex, preste atenção ao aviso quando ele disparar, e revisite os quantificadores e classes que tornam um padrão específico o bastante para continuar rápido.