Ok!
Na primeira parte deste artigo vimos o que é uma expressão regular e o que é o ReDos. Caso você não tenha visitado ainda essa parte convido você a dar uma olhadinha lá para se contextualizar antes de avançar nesta segunda parte do artigo.
Para acessá-la clique AQUI.
Vem comigo que agora vamos resolver esse problema 🙂
Mitigando o risco
Beleza! Sabemos o que é ReDos e como ele é feito agora vamos ver o que podemos fazer para resolver essa situação.
Existem algumas soluções possíveis, como por exemplo validar a expressão regular para ver se ela é vulnerável ou não à esse tipo de ataque. Porém é muito difícil ter 100% de certeza de que uma dada expressão é 100% segura pois esse ataque se baseia na entrada enviada pelo usuário e existem infinitas possibilidades para isso.
Também podemos tentar limitar o tamanho do input mas isso também é ineficaz, existem padrões de regex que podem travar com strings de tamanho 3 ou até menor! Então o que fazer ? Simples!
Vamos utilizar uma micro vm para compilar o código da regex em outra thread fora da thread principal para que assim nossa aplicação não seja comprometida em caso de recebermos um payload que não podemos avaliar.
Mas chega de falar!! Vem comigo que vou te mostrar isso na prática.
Projeto de testes
Para testar essa situação vamos utilizar uma API simples construída em NodeJs utilizando o framework minimalista Express.
Mais informações sobre o Express AQUI.
Será uma API que terá como objetivo validar o formato de um e-mail e de uma senha utilizando para isso expressões regulares.
Ela possui 3 endpoints, sendo eles:
/validate-form-unsafe: Esse endpoint utiliza as formas *erradas de se utilizar regex para fazer validações. Será utilizado para mostrar como isso pode afetar negativamente o sistema.
/validate-form-safe: Esse endpoint será utilizado para testar se nossa proposta de solução de fato funciona.
/test-server: Esse endpoint será utilizado para testar a responsividade do nosso servidor.
Lembrando que é apenas uma API de exemplo então existem coisas que podem ser melhor trabalhas sim, mas não daremos foco nisso, ok ?
Caso queira baixar ou fazer clone do repositório é só clicar AQUI
No nosso exemplo vamos utilizar as seguintes regex's:
/^([a-zA-Z0-9])(([\-.]|[_]+)?([a-zA-Z0-9]+))*(@){1}[a-z0-9]+[.]{1}(([a-z]{2,3})|([a-z]{2,3}[.]{1}[a-z]{2,3}))$/"gm"
Cujo objetivo é validar se uma data string é um e-mail válido e essa outra regex:
/^(([a-z])+.)+[A-Z]([a-z])+$/
Que é utilizada para verificar se a senha informada está no formato de letra intercalada com ponto, por exemplo "a.b.c.zZ" ou "ab.cc.Zd"
Testes
Quando testamos o endpoint /validate-form-unsafe para uma entrada válida, por exemplo:
{
"email":"emailvalido@gmail.com",
"password":"a.b.Cz"
}
```
Tudo flui normalmente.
Porém se enviamos um payload malicioso, por exemplo:
```
{
"email":"email-valido@gmail.com",
"password":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}
```
Isso é o que ocorre:
![Server not responding](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/19y09duwg7iq99kjpv4h.PNG)
Consultando o endpoint **/test-server** verificamos que nossa API simplesmente não funciona mais 🙂.
![server offline](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/98f76jeoaol1zoesbnhq.PNG)
**Mas afinal, o que aconteceu ?**
Bem, vamos analisar o código atrás de respostas. A função que utiliza regex para validar, de forma errada, é essa:
```
function unsafeValidateForm(email, password) {
const isEmailValid = validadeEmailRegex.exec(email) !== null;
const isPasswordValid = validadePasswordRegex.exec(password) !== null;
return isEmailValid && isPasswordValid;
}
```
O fato de essas expressões estarem sendo avaliadas na thread principal que é o problema. Pois dessa forma, devido ao comportamento de **catastrophic evaluation (Avaliação catastrófica)** ou **catastrophic backtracking( Retorno catastrófico)** o nodeJs fica stagnado alí sem conseguir avançar, com isso ele não consegue atingir as outras fases do EventLoop, não conseguindo processar as requisições que chegam, causando assim a interrupção dos serviços. Lembrando que explico mais detalhes sobre o que é o EventLoop, quais são suas fases e porque não podemos bloqueá-lo de forma alguma no meu outro artigo aqui da dev.to, [clique aqui para acessá-lo](https://dev.to/r9n/entendendo-e-solucionando-o-bloqueio-do-event-loop-no-nodejs-parte-1-3nec)
**Solução**
Bem, e o que podemos fazer para resolver isso ?
Vamos usar um recurso muito bacana para isso que são as node vm's
Caso você não saiba o que é isso, aqui vai uma explicação bem sucinta:
**vm** é um mecanismo através do qual o NodeJs consegue compilar e executar código javascript em um contexto totalmente separado do escopo atual de onde ele é chamado mantendo utilizando o motor da v8, que é sobre o qual ele própio é executado, ainda assim, a capacidade de acessar e modificar variáveis locais. Cada vm possui sua própria thread de execução, executando de forma independente da thread principal.
[Mais sobre vm's](https://nodejs.org/api/vm.html#vm-executing-javascript)
A seguir temos a versão repaginada da nossa função de validação de senha.
```
async function safeValidateForm(email, password) {
try {
validateData(email, password);
const vmVariables = {
regexResult: false,
};
const context = vm.createContext(vmVariables);
const regexScript = new vm.Script(
`regexResult = new RegExp(${validadePasswordRegex}, "gm").exec("${password}") !== null &&
RegExp(${validadeEmailRegex},"gm").exec("${email}") !== null`
);
regexScript.runInContext(context, {
timeout: maxRegexEvaluationTimeoutInMillissecons,
});
return vmVariables.regexResult !== null;
} catch (error) {
if (error.code == InternalErrorMessages.TIMEOUT) {
throw new TimeoutError("ValidateForm");
}
throw error;
}
}
```
Essa abordagem tem por princípio executar essa verificação em uma thread separa, por isso usamos aqui uma vm para tal, para assim não corrermos o risco de termos a nossa thread principal bloqueada, levando assim a um completo travamento da nossa API.
Vamos analisar a função nova e verificar o que ela faz. Logo no inicio temos o seguinte trecho de código:
```
try {
validateData(email, password);
const vmVariables = {
regexResult: false,
};
const context = vm.createContext(vmVariables);
```
Aqui nós estamos criando um novo contexto de execução para ser utilizado na vm que vamos utilizar. Caso quiséssemos passar algum valor do contexto atual para a vm seria nessa etapa que deveríamos inserir no contexto criado a informação que quiséssemos injetar.
**!!!ATENÇÃO!!!**
Inputs recebidos do usuário **NUNCA** devem ser passados diretamente para o sistema sem antes receber algum tratamento. A função **validateData** que fiz aqui está vazia pois ela é apenas para lhe lembrar que em produção você deve **sempre** tratar os inputs recebidos, ok ?
Aqui não vou me ater a isso pois o foco é outro.
A seguir temos o seguinte código:
```
const regexScript = new vm.Script(
`regexResult = new RegExp(${validadePasswordRegex}, "gm").exec("${password}") !== null &&
RegExp(${validadeEmailRegex},"gm").exec("${email}") !== null`
);
```
Aqui passamos para a vm em forma de string o script que será executado. Repare que esse scrip é justamente a avaliação das expressões regulares que precisamos, justamente com os parâmetros de **email** e **password** que recebemos do usuário e precisamos avaliar.
Repare que no script temos o seguinte trecho "regexResult = new Re..." esse **regexResult** é o mesmo que declaramos no contexto e é ele quem vai receber o valor do resultado da nossa operação. Quando o processamento dessa micro vm terminar nós poderemos acessar esse resultado fazendo **vmVariables.regexResult**, legal né ? 🙂
Por fim temos o trecho de código:
```
regexScript.runInContext(context, {
timeout: maxRegexEvaluationTimeoutInMillissecons,
});
return vmVariables.regexResult !== null;
} catch (error) {
if (error.code == InternalErrorMessages.TIMEOUT) {
throw new TimeoutError("ValidateForm");
}
throw error;
}
```
Aqui nós executamos o script criado logo acima na micro vm utilizando o contexto criado e então podemos acessar o resultado da execução das regex's fazendo **vmVariables.regexResult**.
O importante aqui é notar a propriedade **timeout** que é justamente o que nos faz conseguir continuar processando coisas, mesmo que essa micro vm acabe travando pois ao se chegar ao tempo limite definido em **maxRegexEvaluationTimeoutInMillissecons**, que no nosso caso está configurado para 50, a vm é morta e um erro é lançado. Esse erro lançado e capturado no bloco de catch e então fazemos o tratamento dele, lançando um erro personalizado de timeout.
E basicamente é assim que podemos tratar uma regex que não sabemos se irá travar ou não, ou até mesmo outra task que poderia comprometer nossa aplicação.
Agora que vimos como podemos resolver esse problema vamos testar nossa solução e ver se ela de fato funciona.
Mas isso fica para a parte 3 pois por aqui já foram muitas informações. Abaixo você encontra o link para a terceira e ultima parte desse artigo. Te espero lá 🙂.
[TerceiraParte](https://dev.to/r9n/protegendo-sua-api-nodejs-contra-redos-attackparte-3-40g7)
Top comments (0)