ReDoS: O Denial of Service que Mora na sua Regex


Introdução
Esses dias descobri que cerca de 5% das vulnerabilidades de segurança em aplicações Node.js são causadas por ReDoS e isso me surpreendeu.
Também conhecido como Regular Expression Denial of Service, o ReDoS é um tipo de vulnerabilidade que explora expressões regulares mal construídas para causar negação de serviço (DoS). Ela aparece com frequência em CVEs, mas como pentester, quase nunca coloco esse tipo de falha em prática. Isso porque o impacto geralmente está relacionado à disponibilidade do serviço, o que costuma estar fora do escopo das operações de teste.
Como eu sempre via esse tipo de falha por aí, mas nunca tinha parado pra entender de verdade como ela funciona, resolvi entender um pouco mais do assunto.
Conceito
Antes de mais nada, vamos entender o que é uma expressão regular. Também conhecidas como RegEx, elas são sequências de caracteres que definem um padrão a ser encontrado em um texto, normalmente usadas para validação de dados.
Por exemplo: quando você digita algo em um campo de e-mail, a aplicação verifica se aquele texto segue o padrão de um endereço de e-mail usando uma expressão regular. Outros exemplos comuns incluem validação de datas, CPFs ou CEPs.
Um ataque de ReDoS acontece quando um atacante explora uma funcionalidade da aplicação que interpreta expressões regulares, com o objetivo de forçar a função a consumir muitos recursos, deixando o sistema lento ou até fora do ar.
Na maioria das linguagens de programação, as engines de expressões regulares usam autômatos finitos não determinísticos (NFA). Isso significa que, ao tentar casar um texto com uma expressão, o motor da regex pode precisar testar vários caminhos possíveis até encontrar (ou não) uma correspondência.
A lógica funciona mais ou menos assim: a engine tenta fazer o primeiro match com o primeiro caractere e segue a partir daí. Se algo falhar no meio do caminho, ela começa a retroceder (o que chamamos de backtracking) e tenta outras combinações, começando do último ponto onde poderia ter seguido por uma rota diferente.
O problema é que, quando usamos operadores de repetição como +
ou *
, o número de caminhos possíveis aumenta exponencialmente. Isso porque as engines de regex são geralmente gananciosas (greedy), ou seja, tentam consumir o máximo de caracteres que puderem antes de desistir.
Agora imagina uma situação onde a regex quase encontra um match, mas falha só no final e isso acontece com uma entrada gigantesca. A engine vai realizar milhares (ou milhões) de tentativas antes de desistir, em um fenômeno chamado Catastrophic Backtracking e é exatamente isso que quebra a aplicação.
Se tudo isso ainda parece meio confuso, calma que vou tentar exemplificar a seguir.
Exemplo
Para explorar uma vulnerabilidade de ReDoS, o atacante geralmente segue por dois caminhos:
Enviar um input malicioso para uma regex vulnerável, conhecida como Evil Regex)
Criar a própria expressão regular vulnerável, caso a aplicação permita que o usuário defina regex personalizadas.
Vamos começar com o primeiro caso, que é o mais comum.
Para isso, vamos inventar uma expressão regular vulnerável como exemplo: B(I|D+)+O
.
A título de curiosidade, o NFA dessa regex ficaria parecido com a imagem abaixo, mas não vamos entrar nos detalhes de como interpretá-lo agora.
O que nossa expressão procura é uma palavra que comece com B
, seguida de uma ou mais ocorrências da letra I
ou de uma ou mais ocorrências da letra D
, e que termine com O
.
O trecho (I|D+)+
significa exatamente isso: "um ou mais grupos contendo I
ou uma ou mais letras D
".
Note como o +
está sendo aplicado duas vezes, tanto dentro quanto fora do grupo, o que contribui diretamente para a complexidade da regex e abre espaço para o backtracking excessivo.
Para analisar expressões regulares de forma prática, podemos usar o site regex101.
Alguns exemplos de palavras que casam com a expressão e quantas etapas (steps) o mecanismo leva para analisá-las:
BIDO
— 11 stepsBIDDDO
— 11 stepsBDIDIDIO
— 21 steps
Agora, exemplos de palavras que não casam com a expressão, mas que ainda assim exigem processamento pesado:
BIDX
— 16 stepsBIDDX
— 25 stepsBDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDX
— Timeout
Você pode verificar a quantidade de steps no regex101 clicando em “Try lauching the debugger” no painel lateral.
Se testarmos o payload de timeout, observamos que de fato ele acontece.
Nitidamente, o problema está na expressão (I|D+)+
. Esse agrupamento de quantificadores causa o catastrophic backtracking. O mecanismo de regex precisa testar várias formas de agrupar os caracteres da string DDDD...
para tentar casar com a expressão.
Quando o algoritmo de expressões regulares encontra algo como B
+ vários D
s + um final que falha (como a letra X
nesse caso), ele tenta todas as combinações possíveis de divisão dos D
s entre I
e D+
.
Cada divisão é um novo caminho a ser testado. E se o final não corresponder (por exemplo, se o O
estiver ausente ou inválido), ele vai retroceder e tentar todas as outras possíveis combinações.
Tentativas
B
+DDD
comoD+
→ tenta casarO
comX
❌B
+DD
comoD+
,D
comoI
→ ❌B
+D
comoD+
,DD
comoD+
→ tenta casarO
comX
❌… (e assim por diante)
Conforme o número de D
s aumenta, o número de possíveis divisões cresce exponencialmente, e a engine tenta todas antes de desistir.
Aqui segue uma lista de outras possíveis expressões regulares vulneráveis:
(a+)+$
([a-zA-Z]+)*$
(a|aa)+$
(a|a?)+$
/(a+)+/
/([a-zA-Z]+)*/
/(a|aa)+/
/(a|a?)+/
O exemplo abaixo representa o segundo cenário de ataque, onde o programa está criando a regex dinamicamente com base no campo username:
String userName = textBox1.Text;
String password = textBox2.Text;
Regex testPassword = new Regex(userName);
Match match = testPassword.Match(password);
if (match.Success)
{
MessageBox.Show("Não inclua o username na senha!");
}
else
{
MessageBox.Show("Senha válida.");
}
Se um atacante usar ^([a-z]+)+$
como username
e aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaA
como password
, o programa vai eventualmente travar.
O problema está na parte ([a-z]+)+
da expressão:
Com esse payload, o
password
quase casa com a regex^([a-z]+)+$
, exceto peloA
maiúsculo no final.A regex engine tenta todas as formas possíveis de agrupar os vários
a
dentro do[a-z]+
, tentando encontrar uma correspondência.Mas sempre falha no final, porque
A
não está dentro de[a-z]
.
Isso leva ao Catastrophic Backtracking, causando um consumo absurdo de CPU até que o regex desista.
Inclusive, você pode testar essa combinação no regex101 e observar o timeout.
Node.js
O modelo de execução single-threaded do JavaScript, onde todas as requisições são gerenciadas pela mesma thread, faz com que aplicações NodeJS sejam constantemente afetadas por ReDoS.
Para evitar esse tipo de problema, desenvolvedores tentam dividir computações de longa duração em eventos menores, os quais são gerenciados de forma assíncrona. O problema é que, nas engines atuais de JavaScript, a comparação de uma string com uma regex não pode ser facilmente dividida em múltiplos blocos de computação.
Dessa forma, uma simples requisição pode congelar a thread principal, fazendo com que a aplicação inteira fique não responsiva. É por isso que grande parte das CVEs relacionadas a essa vulnerabilidade são encontradas em aplicações NodeJS/JavaScript.
Isso se torna um pouco mais difícil quando estamos lidando com outras linguagens, como Java, por exemplo, já que essa tecnologia trabalha com múltiplas threads para diferentes requisições, o que diminui o impacto de um eventual travamento isolado.
Caso Real
Procurando por uma vulnerabilidade recente de ReDoS, encontrei a CVE-2025-26042, que afeta o Uptime Kuma, uma ferramenta open source escrita em JavaScript.
A vulnerabilidade foi corrigida, e você pode conferir o pull request com o código vulnerável aqui:
👉 github.com/louislam/uptime-kuma/pull/5563/files
O trecho vulnerável era a seguinte expressão regular: /^([\d\.,]+)\s?(\w+)$/
.
Quebrando a expressão em partes para melhores entender:
Parte | Significado |
^ | Início da string |
([\d\.,]+) | Grupo 1: um ou mais dígitos, pontos ou vírgulas |
\s? | Zero ou um espaço |
(\w+) | Grupo 2: uma ou mais letras, números ou _ |
$ | Fim da string |
Nesse cenário, se o input for composto por milhares de vírgulas ou pontos como abaixo, a engine entra em colpaso.
','.repeat(50000) + '\x0000'
Isso porque ela tenta dividir a sequência gigantesca de ,
de todas as formas possíveis, tentando encontrar uma combinação que permita que o restante da regex também case, nesse caso, o \s?(\w+)
no final.
Só que como o final (\x00
) não é um caractere válido para \w+
, nenhuma das tentativas dá match, e a engine continua tentando infinitamente dividir o grupo anterior até desistir (ou travar o servidor). Esse é mais um caso clássico de catastrophic backtracking.
Vamos supor o seguinte payload malicioso:
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,\\x00
O comportamento seria o seguinte:
A regex engine vê a repetição de
,
como um possível match para[\d\.,]+
, então ela entra no modo de backtracking.Depois disso, ela tenta casar o restante da expressão:
\x00
não é um espaço, então\s
falha.Mas como
\s?
aceita zero ocorrências, a engine simplesmente ignora essa parte e continua.Em seguida, tenta casar
(\w+)
com\x00
, mas falha, porque\x00
não é um caractere considerado "word character".
Nesse ponto, a engine volta e tenta uma nova divisão de
([\d\.,]+)
para ver se o restante (\s?(\w+)
) pode casar com outra configuração.Como estamos lidando com muitas vírgulas, ela testa todas as divisões possíveis e aí vem o comportamento exponencial que consome recursos da aplicação e trava o processo.
Se a gente colocasse um a
no final do payload, tipo:
A regex encontraria um match logo de cara, sem precisar fazer backtracking, o que mostra como uma pequena mudança pode impactar drasticamente o comportamento da engine.
Abordagem de Pentest
Afinal, como descobrir se uma aplicação está vulnerável a ReDoS sem afetar a disponibilidade do sistema?
Tudo depende do que foi acordado nas regras de engajamento do pentest. Em muitos casos, especialmente em ambientes de produção, não é permitido realizar testes que impactem a performance ou disponibilidade da aplicação e isso inclui ReDoS.
Se você tem permissão para testar, a ideia é aplicar os payloads de forma gradativa e controlada. Vá observando se o tempo de resposta aumenta proporcionalmente ao tamanho do input, mas sem causar indisponibilidade real. Isso já pode ser um forte indicativo de vulnerabilidade.
Outro cenário possível: se você estiver em um ambiente de staging ou desenvolvimento, vale alinhar com a equipe responsável uma janela de teste específica. Assim, travar ou derrubar a aplicação por alguns minutos não causaria impacto direto.
Se o teste for white-box e você tem acesso ao código-fonte, pode explorar a aplicação localmente ou usar ferramentas (mostraremos mais abaixo) para validar se uma regex é potencialmente vulnerável. Da mesma forma, se estiver analisando uma biblioteca JavaScript (ou qualquer outra), você pode simplesmente baixá-la, rodar localmente e testar seus próprios payloads de forma segura.
Ferramentas
Procurando por ferramentas que ajudam a identificar vulnerabilidades de ReDoS, encontrei três bem interessantes:
Testei a ReDoS Checker e a ReScue, e ambas pareceram entregar resultados bem consistentes.
Um ponto positivo é que a ReDos Checker é uma ferramenta web, então fica super prático de usar, basta colar a regex e ela já analisa se há risco de backtracking.
Rescue identificando a vulnerabilidade na nossa regex de exemplo:
Rescue detectando a regex usada na CVE-2025-26042:
Aliás, o payload que usamos para demonstrar o comportamento vulnerável da CVE foi gerado por essa ferramenta.
Acabei não testando nenhuma ferramenta de SAST pra detectar ReDoS, mas é bem provável que soluções como o Semgrep consigam detectar esse tipo de padrão no código.
Remediação
A maneira mais simples de evitar um ReDoS seria remover qualquer validação com regex, mas na maioria dos casos isso não é viável.
Quando isso não for possível, a melhor abordagem é evitar a criação de padrões considerados "Evil Regex", ou seja, aqueles que causam catastrophic backtracking. Em especial: evite quantificadores aninhados, como ou +
dentro de outro +
, pois esse tipo de construção é o principal causador do problema.
Voltando ao nosso exemplo didático, a regex vulnerável B(I|D+)+O
poderia ser reescrita de forma segura como:
BI+O|BD+O
Essa nova expressão ainda atende ao propósito de casar palavras como BIDO
, BIIDO
, ou BIDDO
, mas sem aninhar quantificadores.
No caso real da CVE-2025-26042, a regex vulnerável era:
/^([\d\.,]+)\s?(\w+)$/
E foi substituída por:
/^([\d\.,]+)\s?([a-zA-Z]+)$/
A parte vulnerável era \w+
, que permite casar letras, números e underscore ([a-zA-Z0-9_]
). Com isso, a engine tinha um leque muito amplo de possibilidades para tentar combinar com o que vinha após a repetição [\d\.,]+
, o que abria margem para o backtracking com certos inputs.
Ao trocar \w+
por [a-zA-Z]+
, a regex:
Restringe os caracteres válidos para apenas letras (maiúsculas e minúsculas).
Reduz o número de combinações possíveis que a engine tenta, porque como as vírgulas não fazem parte de
[a-zA-Z]
, a engine não vai tentar mover vírgulas para esse grupo.Evita o comportamento exponencial, já que o padrão final fica mais específico e fácil de validar.
Essa mudança torna a expressão mais previsível e menos propensa a causar backtracking excessivo, mitigando assim o risco de ReDoS.
Também é possível configurar timeouts para expressões regulares. Ou seja, se a avaliação demorar mais que um certo tempo (como 1 segundo, por exemplo), ela é automaticamente interrompida. Isso ajuda a evitar que a aplicação congele completamente.
Por fim, uma forma prática de atenuar o problema, mesmo sem resolver a raiz da vulnerabilidade, é limitar a quantidade de caracteres no campo que será analisado pela regex. Isso reduz o espaço para que um payload malicioso cresça o suficiente a ponto de causar impacto significativo.
Conclusão
A vulnerabilidade ReDoS continua sendo uma ameaça real e recorrente no mercado, afetando diretamente a disponibilidade das aplicações, especialmente em ambientes onde o servidor opera de forma single-threaded, como no Node.js.
O mais preocupante é que criar uma expressão regular vulnerável é muito fácil, e na maioria dos casos, o desenvolvedor nem imagina que uma simples regex pode abrir brechas para ataques desse tipo. Embora identificar uma ReDoS nem sempre seja trivial, existem hoje ferramentas que facilitam bastante essa análise, permitindo validar se uma expressão apresenta comportamento malicioso ou não.
Espero que esse conteúdo tenha te ajudado a entender melhor esse tipo de falha e como evitá-la no seu código. Obrigado e até mais =)
Links e Referências
Subscribe to my newsletter
Read articles from b1d0ws directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
