Testar regex
O Testador de Regex aplica um padrão a um texto no seu navegador, dentro de um Web Worker com tempo limite de dois segundos. Isso não é cautela à toa: o JavaScript não oferece nenhuma forma de interromper uma expressão regular em execução, então um padrão como (a+)+$ contra uma entrada longa prende a thread indefinidamente, e encerrar o worker é a única defesa que realmente funciona.
Como funciona o Testar regex?
A maioria dos testadores de regex executa o padrão na thread principal da página. Isso funciona até alguém colar um padrão que faz backtracking, momento em que a aba para de responder e a única saída é fechá-la.
Backtracking catastrófico e por que ele não pode ser detectado
O motor de regex do JavaScript faz backtracking: quando uma correspondência falha, ele tenta de novo cada caminho alternativo. Para (a+)+b contra uma sequência de caracteres a terminada em qualquer outra coisa, os caminhos são todas as maneiras de dividir a sequência entre os dois quantificadores — o que é exponencial. Trinta caracteres significam cerca de um bilhão de tentativas. Não há opção de tempo limite, sinal de aborto, ponto de pausa ou exceção; o motor simplesmente não retorna.
Essa é a classe de vulnerabilidade ReDoS, e ela derruba sistemas reais. A interrupção global da Cloudflare em 2019 foi um regex em uma regra de WAF; a da Stack Overflow em 2016 foi um padrão que casa espaços em branco em um post com uma longa sequência deles. Ambos os casos ocorreram em um servidor, onde a consequência é toda requisição travar, e não apenas uma aba.
Um worker pode ser encerrado, e é essa toda a arquitetura
Por isso a correspondência roda em um Web Worker, e um temporizador o encerra após dois segundos. Esse é o único mecanismo disponível: nada consegue parar um regex no meio da execução, mas uma thread pode ser destruída. Quando o tempo limite é atingido, você recebe uma mensagem clara indicando a causa provável em vez de uma aba congelada — o que também é um sinal útil, porque um padrão que estoura o tempo aqui é um padrão que travaria um servidor.
A análise estática descreve estruturas, não vereditos
O painel de risco procura quantificadores aninhados, alternâncias quantificadas com ramos sobrepostos e quantificadores adjacentes sobre os mesmos caracteres. Esses formatos explicam praticamente todos os incidentes reais. O que ele deliberadamente não faz é declarar um padrão seguro: determinar se um regex arbitrário tem comportamento exponencial no pior caso é um problema de análise de verdade, e uma ferramenta que prometesse segurança estaria fazendo uma promessa que não pode cumprir.
Correspondências de comprimento zero precisam de um empurrão
Um loop de correspondência global sobre um padrão que pode não casar com nada — a*, (?:), \b — nunca avança o lastIndex e entra em loop infinito. Toda implementação correta o incrementa manualmente após uma correspondência vazia, e esta o faz; é um detalhe pequeno que quebra um número surpreendente de ferramentas.
A decomposição é feita por token
O padrão é dividido em seus tokens, com uma nota em linguagem simples para cada um, porque o problema comum de um regex escrito por outra pessoa não é estar errado, e sim ser ilegível. Sequências de caracteres literais são agrupadas em vez de listadas uma a uma.
Padrão
(\w+)@(\w+\.\w+)
Correspondências
1 [email protected] at 58 $1 ada $2 example.com 2 [email protected] at 271 $1 grace $2 example.org
Quais opções e casos-limite o Testar regex suporta?
| Parâmetro | Tipo | Padrão | Comportamento e casos-limite |
|---|---|---|---|
| g — global | flag | ativada | Encontra todas as correspondências em vez de apenas a primeira. Sem ele, é devolvido um único resultado, não importa quantos existam. |
| i — ignorar maiúsculas e minúsculas | flag | desativada | Sem diferenciar maiúsculas de minúsculas. Com a flag u, também aplica o case folding do Unicode, que cobre mais que ASCII. |
| m — multilinha | flag | ativada | ^ e $ correspondem em cada quebra de linha, e não apenas nas extremidades do texto. É quase sempre o que você quer em entradas com cara de log. |
| s — dotAll | flag | desativada | Faz o . corresponder também a uma quebra de linha. Sem ela, o .* para no fim da linha, que é o motivo usual de uma correspondência multilinha falhar. |
| u — Unicode | flag | desativada | Ativa os escapes \u{…} e as classes de propriedades \p{…}, e faz o motor trabalhar em pontos de código em vez de unidades UTF-16 — é isso que faz o . corresponder a um emoji inteiro. |
| y — sticky | flag | desativada | Corresponde apenas em lastIndex, sem avançar em busca de correspondências. Usado em tokenizadores, onde uma lacuna significa erro de sintaxe, e não algo a pular. |
| Tempo limite | milissegundos | 2000 | Depois disso, o worker é encerrado. Não há como distinguir de antemão um padrão lento de um exponencial, então o tempo decorrido é o único sinal disponível. |
| Limite de correspondências | contagem | 5000 | Acima disso, os resultados são truncados, e o truncamento é informado. Renderizar cinquenta mil linhas não ajuda ninguém. |
| Substituição | $1, $<name>, $& | — | Grupos numerados, grupos nomeados e a correspondência inteira. $$ é um cifrão literal, que é o que todo mundo esquece. |
Perguntas frequentes
Por que meu padrão estourou o tempo limite?
Quase certamente ele está fazendo backtracking catastrófico. Procure um quantificador dentro de um grupo quantificado — (a+)+, (\d*)* — ou uma alternância quantificada cujos ramos possam casar com o mesmo texto, como (a|ab)+. O motor tenta todas as maneiras de dividir a entrada entre eles, o que é exponencial no comprimento da entrada. O painel de risco descreve a estrutura, se conseguir encontrar uma.
Como corrigir um padrão que faz backtracking?
Elimine a ambiguidade sobre qual parte casa com o quê. Troque (a+)+ por a+. Torne os ramos da alternância mutuamente exclusivos, de modo que apenas um possa casar em cada posição. Ancore o padrão, para que uma falha não recomece de cada caractere. Limite as repetições abertas ao que os dados realmente contêm. E prefira uma classe de caracteres específica a um curinga: [^"]* faz muito menos backtracking que .*.
ReDoS é um risco real ou teórico?
Real, e caro. A interrupção global da Cloudflare em julho de 2019 foi uma única expressão regular em uma regra de WAF; a Stack Overflow saiu do ar em 2016 por causa de um padrão que casa espaços à direita quando um post continha uma longa sequência deles. Em um servidor, o modo de falha é pior do que aqui — toda requisição entra na fila atrás daquela travada no motor, então uma única entrada maliciosa derruba o serviço.
Por que meu .* não atravessa linhas?
Porque o . não casa com quebra de linha, a menos que a flag s esteja ativa. Essa é a maior surpresa de regex em JavaScript, e é fácil confundi-la com a flag m — mas m muda o que ^ e $ significam, não o que o . casa. Para uma correspondência multilinha você quer s; para ancorar em linhas você quer m; elas são independentes e, muitas vezes, você quer as duas.
Devo validar um endereço de e-mail com um regex?
Apenas de forma flexível. A gramática do RFC 5322 para um endereço, como padrão, tem várias centenas de caracteres, aceita coisas que ninguém espera — partes locais entre aspas, comentários, domínios com IP literal — e ainda assim não consegue dizer se a caixa de correio existe. O padrão útil verifica um @ com algo dos dois lados e um ponto no domínio; a validação de verdade é enviar um e-mail de confirmação.
Meu padrão ou o texto testado é enviado?
Não. A correspondência roda em um Web Worker dentro do seu navegador — uma thread separada na mesma aba, não um servidor. Você pode confirmar isso no painel Network. Isso importa porque o texto contra o qual as pessoas testam costuma ser um log de produção, e logs de produção contêm endereços de e-mail, tokens e identificadores de clientes.