Prevenir ReDoS no Node.js é essencial quando a aplicação usa expressões regulares em dados controlados pelo usuário. ReDoS significa Regular Expression Denial of Service e acontece quando uma regex exige tempo excessivo para decidir se uma entrada corresponde ao padrão.
O problema costuma aparecer em expressões com quantificadores aninhados, alternativas ambíguas e backtracking catastrófico. Como JavaScript executa a regex no mesmo processo, uma única entrada maliciosa pode bloquear o event loop e atrasar todas as requisições atendidas pela instância.
Neste guia, você aprenderá como identificar padrões perigosos, substituir regex complexas, limitar input, usar analisadores seguros, aplicar timeouts por isolamento, testar casos adversariais e monitorar o event loop.
O que é ReDoS?
ReDoS explora o custo computacional de uma expressão regular. A referência da OWASP sobre ReDoS explica o ataque. A documentação de expressões regulares da MDN apresenta a sintaxe usada no JavaScript.
Backtracking
Motores de regex podem tentar diferentes caminhos quando uma correspondência falha. Em padrões ambíguos, a quantidade de combinações cresce rapidamente.
Exemplo perigoso
const pattern = /^(a+)+$/;
pattern.test('aaaaaaaaaaaaaaaaaaaaaaaa!');O padrão possui um quantificador dentro de outro. Quando o caractere final não corresponde, o motor pode testar muitas divisões possíveis da sequência de letras.
Outro padrão ambíguo
/^(a|aa)+$/As alternativas se sobrepõem. A mesma sequência pode ser dividida de muitas maneiras.
Impacto no event loop
Uma regex síncrona bloqueia a thread JavaScript. Enquanto ela executa, timers, conexões e outras requisições aguardam.
Para entender esse comportamento, consulte Event Loop no Node.js.
Entrada controlada pelo usuário
Os principais riscos aparecem em:
- validação de e-mail e URL;
- filtros de busca;
- parsers de logs;
- rotas com padrões dinâmicos;
- templates;
- sanitização de HTML;
- regras configuráveis por clientes.
Regex dinâmica
const regex = new RegExp(req.query.pattern);
const matched = regex.test(documentText);Permitir que o usuário forneça o padrão completo é ainda mais perigoso. Além de ReDoS, o padrão pode consumir memória e produzir resultados inesperados.
Não aceite regex arbitrária
Prefira filtros estruturados, operadores predefinidos e busca textual do banco.
{
"field": "name",
"operator": "contains",
"value": "ana"
}A aplicação converte operadores permitidos em uma consulta segura.
Limite de tamanho
Mesmo uma regex razoável pode ficar cara em texto enorme. Limite o input antes da avaliação:
if (Buffer.byteLength(input, 'utf8') > 10_000) {
throw new ValidationError('Entrada muito grande');
}Limite do padrão
Se uma funcionalidade realmente aceita padrões, limite tamanho, recursos e quantidade de grupos. Ainda assim, uma análise completa de segurança é difícil.
Evite quantificadores aninhados
Padrões como:
(a+)+
(.*)*
(\w*)+
merecem revisão. Reescreva para uma única repetição quando possível.
Alternativas sobrepostas
Evite:
(a|aa)+
(\d|\d\d)*Prefira uma forma não ambígua, como a+ ou um parser explícito.
Wildcards amplos
Padrões com .* entre delimitadores repetidos podem causar muito backtracking:
^.*BEGIN.*END.*$Use limites mais específicos e análise em etapas.
Validação de e-mail
Não tente implementar toda a especificação de e-mail com uma regex gigantesca. Use uma validação simples de formato, limite de tamanho e, quando necessário, confirmação por e-mail.
Validação de URL
Use a classe URL, não uma regex complexa:
let parsed;
try {
parsed = new URL(value);
} catch {
throw new ValidationError('URL inválida');
}Para URLs externas, combine com as defesas de SSRF no Node.js.
Validação de números
Converta e valide tipos:
const value = Number(input);
if (!Number.isFinite(value)) {
throw new ValidationError('Número inválido');
}Não use regex para tudo.
Parsers específicos
Datas, URLs, JSON e identificadores possuem parsers ou validadores dedicados. Eles normalmente são mais legíveis e previsíveis que padrões extensos.
Regex simples
Um padrão ancorado e limitado pode ser adequado:
/^[a-z0-9_-]{1,64}$/iO quantificador possui limite e a classe não é ambígua.
Anchors
^ e $ reduzem buscas em posições diferentes quando a intenção é validar a string inteira. Eles não corrigem backtracking interno, mas evitam trabalho desnecessário.
Limites explícitos
Prefira:
/^[a-z]{1,100}$/em vez de:
/^[a-z]+$/quando existe um limite de negócio.
Flags
Entenda o impacto de g, m, s e u. Uma regex global mantém lastIndex, o que pode causar bugs quando a instância é reutilizada.
Unicode
Caracteres Unicode, combinações e pares substitutos podem aumentar complexidade. Normalize apenas quando o requisito é claro e limite o tamanho em bytes e caracteres.
Static analysis
Ferramentas como safe-regex e scanners SAST podem identificar padrões suspeitos. Elas geram falsos positivos e negativos, portanto precisam de revisão humana.
Dependências
Uma biblioteca pode usar regex vulnerável internamente. Mantenha lockfile e acompanhe advisories. O artigo sobre segurança de dependências será publicado nesta fila.
Testes adversariais
Para cada regex relevante, teste entradas que quase correspondem:
const input = 'a'.repeat(50_000) + '!';O pior caso frequentemente termina com um caractere inválido.
Benchmark
const startedAt = performance.now();
const result = pattern.test(input);
const durationMs = performance.now() - startedAt;
assert.ok(durationMs < 50);Use limites adequados ao ambiente de teste e execute com tamanhos crescentes.
Curva de crescimento
Meça 100, 1.000, 10.000 e 100.000 caracteres. Uma duração que cresce muito mais rápido que o tamanho indica risco.
Timeout não existe na regex nativa
JavaScript não fornece um timeout direto para RegExp.test(). Um setTimeout não interrompe a operação porque o event loop está bloqueado.
Worker Threads
Para padrões não confiáveis, execute em worker isolado e encerre após o prazo:
const worker = new Worker('./regex-worker.js', {
workerData: { pattern, input }
});
const timer = setTimeout(() => {
worker.terminate();
}, 100);Isso limita o bloqueio ao worker, mas ainda exige controle de memória, quantidade de workers e input.
Consulte Worker Threads no Node.js.
Processo separado
Um processo filho oferece isolamento mais forte e pode receber limites de CPU e memória pelo sistema operacional.
Fila de trabalho
Não crie um worker por requisição sem limite. Use pool e rejeite excesso de carga.
RE2
Motores baseados em RE2 evitam backtracking catastrófico ao limitar recursos da linguagem. Eles não suportam todos os recursos de regex, como alguns backreferences e lookarounds.
Avalie bibliotecas mantidas e compatibilidade antes de substituir o motor.
Backreferences
Padrões com referências a grupos podem ser difíceis de analisar e não são suportados por motores lineares. Considere um parser.
Rate limiting
Limite endpoints que executam busca ou validação cara. Consulte Rate Limiting no Node.js.
Timeout da requisição
Timeout HTTP evita conexões infinitas, mas não interrompe a regex já executando. Ele deve ser combinado com eliminação do padrão ou isolamento.
Event loop delay
Monitore atraso do event loop com perf_hooks:
const histogram = monitorEventLoopDelay();
histogram.enable();Veja Performance Hooks no Node.js.
CPU
ReDoS costuma causar CPU alta, latência crescente e poucas requisições concluídas. Configure alertas que correlacionem esses sinais.
Logs
Registre rota, ID da regra, tamanho do input e duração. Não registre o texto completo quando pode conter dados pessoais ou payload enorme.
Consulte Logs com Pino no Node.js.
Auditoria
Alterações em padrões configuráveis devem registrar autor, versão anterior, nova versão e resultado da validação.
Feature flags
Uma regex nova pode ser habilitada gradualmente e desativada rapidamente se métricas piorarem.
Code review
Procure por:
new RegExpcom input externo;- quantificadores aninhados;
- alternativas sobrepostas;
.*repetido;- regex extensa sem testes;
- uso em texto sem limite;
- padrões de dependências antigas.
Teste de rota
test('rejeita entrada acima do limite', async () => {
const response = await request(app)
.post('/validate')
.send({ value: 'a'.repeat(100_000) });
assert.equal(response.status, 400);
});Teste de concorrência
Envie múltiplas entradas adversariais e confirme que o serviço mantém health checks e requisições normais.
Teste de worker
Use um padrão propositalmente caro, confirme término do worker e liberação dos recursos.
Teste de regressão
Quando uma regex vulnerável for corrigida, mantenha a entrada que causava lentidão como fixture permanente.
Erros comuns
- Regex gigante para e-mail: complexidade desnecessária.
- Input sem limite: custo cresce indefinidamente.
- setTimeout ao redor da regex: não interrompe a execução.
- Confiar só em scanner: padrões perigosos passam.
- Regex dinâmica: usuário controla o algoritmo.
- Isolar sem limitar workers: ataque vira exaustão de processos.
- Ignorar dependências: vulnerabilidade continua transitiva.
Boas práticas
- Prefira parsers e validação estruturada.
- Limite tamanho de input.
- Evite quantificadores aninhados.
- Evite alternativas ambíguas.
- Use limites explícitos.
- Teste quase-correspondências.
- Monitore event loop e CPU.
- Isole padrões não confiáveis.
- Atualize dependências.
- Mantenha testes de regressão.
Conclusão
Prevenir ReDoS no Node.js exige avaliar o custo da regex, não apenas se ela retorna o resultado correto. Padrões ambíguos e entradas quase válidas podem bloquear o event loop por tempo suficiente para derrubar a instância.
Validação estruturada, limites, regex simples, testes adversariais e isolamento formam uma defesa efetiva. Quando o serviço monitora event loop e rejeita trabalho não confiável antes de executá-lo, expressões regulares deixam de ser uma porta silenciosa para negação de serviço.


