5 maneiras de evitar a injeção de código em JavaScript e Node.js
6 de abril de 2021
0 minutos de leituraEscrever código JavaScript seguro para evitar a injeção de código pode parecer uma tarefa comum, mas há muitas armadilhas pelo caminho. Por exemplo, o fato de você (desenvolvedor) seguir as melhores práticas de segurança não significa que outras pessoas façam o mesmo. É provável que você use pacotes de código aberto no seu aplicativo. Como saber se eles foram desenvolvidos com segurança? E se houver código inseguro, como eval(), neles? Vamos descobrir.
O que é injeção de código?
A injeção de código é uma forma específica de ataques de injeção, uma categoria mais ampla, em que um invasor pode enviar código JavaScript ou Node.js que será interpretado pelo navegador ou pelo ambiente de execução do Node.js. A vulnerabilidade de segurança ocorre quando o interpretador não consegue distinguir o código confiável pretendido pelo desenvolvedor do código injetado pelo invasor como entrada.
Como evitar a injeção de código
Uma convenção essencial de programação segura é não permitir a execução dinâmica de código no aplicativo. Isso significa evitar construções da linguagem como eval e strings de código passadas para setTimeout() ou para o construtor Function. Em segundo lugar, evite a serialização, que pode ser vulnerável a ataques de injeção que executam código durante o processo. Por fim, faça a análise de dependências para garantir que componentes de código aberto de terceiros não deixem seu aplicativo vulnerável a esse ataque. Além disso, se você usar uma ferramenta de análise estática de código, como o Snyk Code, poderá encontrar possíveis vulnerabilidades de segurança relacionadas à injeção de código no seu código ou no de colegas.
Neste artigo, veremos 5 maneiras de evitar a injeção de código:
Evite
eval(),setTimeout()esetInterval()Evite
new Function()Evite a serialização de código em JavaScript
Use um linter de segurança para Node.js
Use uma ferramenta de análise estática de código (SCA) para encontrar e corrigir problemas de injeção de código
1. Evite eval(), setTimeout() e setInterval()
Sei o que você está pensando: mais um guia mandando evitar eval. É verdade, mas também quero mostrar exemplos reais de outras bibliotecas populares que usaram eval (ou outras formas de construção de código), foram prejudicadas por isso e acabaram causando uma grave vulnerabilidade de segurança.
Antes de vermos os exemplos de pacotes de terceiros vulneráveis, vamos explicar o que é eval e quais funções estão relacionadas a ele. Ambientes de execução JavaScript, como o navegador e a plataforma Node.js no servidor, permitem avaliar e executar código durante a execução. Veja um exemplo prático:
Nesse caso, o programador tenta criar uma maneira dinâmica de acessar dados no DOM. O exemplo pressupõe que getElementform pode ser controlado pelo usuário, assim como a variável elementId. Há maneiras melhores de realizar essa tarefa sem precisar de eval, então, de todo modo, você deve evitar código dinâmico como este.
No Node.js, pode ser necessário permitir o acesso a pontos de dados específicos do aplicativo com base em uma avaliação dinâmica. Veja um exemplo:
Neste exemplo, pressupõe-se que o arquivo específico que queremos importar seja dinâmico e potencialmente controlado pelo usuário, o que pode gerar vulnerabilidades de segurança relacionadas à injeção de código.
A injeção de código no Dustjs mostra um exemplo real de uso inseguro de eval
O pacote npm dustjs do LinkedIn, um projeto de templates assíncronos para o navegador e o Node.js no servidor, mostra como uma vulnerabilidade de injeção de código pode se tornar grave.
Embora esse pacote não seja mais mantido ativamente, ele ainda registra cerca de 72.000 downloads mensais e precisou lidar com uma vulnerabilidade de segurança relacionada à injeção de código.
Os mantenedores do dustjs fizeram o possível para escapar entradas potencialmente perigosas fornecidas pelo usuário, que poderiam chegar a construções de código inseguras como a função eval(). No entanto, a própria função escapeHtml tinha uma falha de segurança: ela verificava apenas se a entrada era uma string e, em seguida, fazia o escape. Também deveria verificar outros tipos, como arrays. Este pull request corrigiu a vulnerabilidade de segurança relacionada à injeção de código:

Você deve estar se perguntando: onde eval() entra nessa história?
Se você usa dustjs, talvez também adicione o pacote npm dustjs-helpers para obter helpers de template adicionais, como operações matemáticas e lógicas. Um desses helpers adicionais é uma condição if, que você pode usar nos seus próprios arquivos de template Dust da seguinte maneira:

Faz sentido, não é?
O problema é que a entrada não controlada do usuário no parâmetro de consulta device é passada diretamente ao helper da condição if, que usa eval, como você pode ver na linha 227, para avaliar a condição dinamicamente:

Agora tudo fica claro: vários problemas de segurança se combinam de uma forma inesperada:
dustjs-linkedin, um pacote de código aberto, tem uma falha de segurança: as strings de entrada não são sanitizadas corretamente na função
escapeHtml.dustjs-helpers, um pacote de código aberto, usa uma prática de programação insegura, como a função
eval(), para avaliar código dinamicamente durante a execução.
Quer ver como explorei essa vulnerabilidade e invadi um aplicativo real em funcionamento usando exatamente essa falha? Confira:

Evite também setTimeout() e setInterval()
Para concluir as boas práticas de evitar eval(), também quero destacar outras funções que você, como desenvolvedor JavaScript, certamente já ouviu falar ou usou pelo menos uma vez no seu aplicativo: setTimeout() e setInterval().
Um fato um pouco menos conhecido sobre essas funções é que elas também aceitam strings de código. Por exemplo, elas podem ser usadas assim:
Ainda bem que literais de string não são permitidos em um ambiente Node.js!
2. Evite new Function()
Outra construção da linguagem, semelhante a eval(), setTimeout() e setInterval(), é o construtor Function, que permite definir uma função dinamicamente com base em literais de string.
Veja este exemplo simples:
Se você acompanhou até aqui, já conhece os possíveis problemas de segurança que podem surgir quando uma entrada do usuário é passada para uma função como essa...
3. Evite a serialização de código em JavaScript
A serialização é bastante comum no ecossistema Java. Meu amigo Brian Vermeer escreveu um artigo sobre como vulnerabilidades de segurança afetam aplicativos Java devido a operações de serialização inseguras. Recomendo muito a leitura: Serialização e desserialização em Java: explicando a vulnerabilidade de desserialização do Java.
Voltando ao universo JavaScript, a serialização também é bastante comum.
É provável que você não escreva sua própria lógica de serialização e desserialização. Mas, no maravilhoso mundo do npm, com mais de 1.500.000 pacotes de código aberto à sua disposição, por que não usá-los?
O js-yaml é muito popular, com mais de 28.000.000 de downloads por semana, e tem uma boa avaliação geral de integridade do pacote no Snyk Advisor:

Dito isso, a captura de tela acima do pacote npm js-yaml mostra que versões anteriores tinham vulnerabilidades de segurança. Qual delas, você pergunta?
Foi constatado que algumas versões do js-yaml eram vulneráveis à execução de código devido à desserialização. A vulnerabilidade ocorre por causa do uso a seguir do construtor new Function():
Veja como seria uma prova de conceito de exploração dessa vulnerabilidade:
Se um agente mal-intencionado conseguir fornecer uma entrada como essa, ou parte dela, usada para criar a variável x no código de prova de conceito acima, uma possível vulnerabilidade poderá se tornar um risco real.
A vulnerabilidade acima remonta a 2013, mas um relatório de segurança de 2019 identificou outro caso de execução arbitrária de código no js-yaml. Portanto, tenha cuidado. Ou, para dar um conselho mais prático e aplicável: evite new Function() e analise seus pacotes de código aberto de terceiros para garantir que você não tenha essas vulnerabilidades e, caso tenha, possa corrigi-las automaticamente com um pull request de correção.
4. Use um linter de segurança para Node.js
Chegando à parte de ferramentas deste guia, vamos falar de linters. Desenvolvedores JavaScript gostam de usar linters. Seja você usuário do standardjs ou do eslint para impor um estilo de código, essas ferramentas são muito comuns em projetos JavaScript e Node.js.
Por que não impor boas práticas de segurança também? É aí que entra o eslint-plugin-security. Seguir as instruções do README para usar o plugin é bem simples. Basta adicionar a configuração do plugin eslint a seguir para ativar a configuração recomendada:
Como o linter ajuda?
Ele tem regras para detectar práticas de programação inseguras, como detect-eval-with-expression, que identifica usos de eval() com expressões ou literais de string. O linter também tem outras regras, como a de uso das APIs child_process do Node.js.
Vale lembrar que a última publicação do eslint-plugin-security foi há mais de 4 anos. Embora ainda possa funcionar bem, você pode considerar pacotes sucessores, como eslint-plugin-security-node.
5. Use uma ferramenta de análise estática de código para encontrar e corrigir problemas de injeção de código
Linters de análise estática de código (SCA), em suas formas mais básicas, como os usados com o ESLint, são um bom ponto de partida. Eles oferecem contexto suficiente para impor um estilo de código, mas, como vimos com o linter de segurança para Node.js, não são flexíveis o bastante para lidar de fato com problemas de segurança.
Algumas preocupações de desenvolvedores com um linter de segurança para Node.js, como o eslint-plugin-security, são:
Falsos positivos: As regras do linter são bastante básicas e podem gerar muitos falsos positivos, aumentando a frustração e a confusão dos desenvolvedores. Por exemplo, a expressão
RegExp(matchEmailRegEx)a seguir fará o linter de segurança para Node.js gerar um erro porque o uso da função RegExp não é literal. Mas talvezmatchEmailRegExseja apenas uma constante no meu arquivo shared/variables.js. O linter não é avançado o suficiente para saber disso.Regras rígidas demais: Dando continuidade ao ponto anterior, a regra é rígida demais. Ou você usa
child_process.exec(someCommand, []), ou não usa. O processo de análise estática de código com o linter não é inteligente o suficiente para identificar quesomeCommandé uma constante definida diretamente no código. O simples fato de usar child_process.exec() com um valor não literal já gera um erro no linter, frustrando os desenvolvedores, que acabam desativando a regra.Regras básicas demais: O conjunto de regras é pequeno, e os problemas identificados são muito básicos. Na prática, é tudo ou nada, sem muito contexto sobre como os dados fluem de uma entrada específica do usuário até um código potencialmente sensível, como a execução de comandos, consultas SQL ou outros.
Reforçando o que eu disse antes: um linter de segurança, como o eslint-plugin-security-node ou outros, é um bom ponto de partida. Com certeza é melhor do que não usar nada.
Mas há maneiras melhores de encontrar problemas de segurança no seu próprio código enquanto você programa.
Quero apresentar a você o Snyk Code, uma ferramenta de teste estático de segurança de aplicações (SAST) criada para desenvolvedores.
Encontrando uma injeção de comandos em um aplicativo Node.js
O Snyk Code será lançado em breve, mas vou mostrar uma prévia de como ele funciona.
Primeiro, conecte-se à Snyk com uma conta do GitHub e importe o repositório do GitHub. Para isso, clique em Add project e depois no ícone do GitHub:

Em seguida, encontre um repositório na lista ou use a caixa de pesquisa para localizá-lo e ative-o para iniciar a análise:

O Snyk importará o repositório do GitHub e fará uma análise rápida.
Ele também detectará automaticamente outros arquivos de manifesto relacionados a possíveis problemas de segurança, como dependências de código aberto com vulnerabilidades conhecidas ou uma imagem Docker que introduza várias vulnerabilidades de segurança.
Vamos nos concentrar no código desta aplicação Node.js. Clique em Code analysis e veja o que encontramos:

O Snyk Code encontrou várias vulnerabilidades. Uma delas é uma vulnerabilidade de injeção de comandos, como vemos aqui:

Os problemas de segurança encontrados nesta linha de código explicam o risco:
Mas como os dados fluem do parâmetro url para a função insegura exec()? Clique no botão detalhes completos para ver uma versão mais detalhada do fluxo de dados e entender melhor o contexto:

Aqui, podemos ver claramente o panorama completo da análise feita pelo Snyk Code.
O parâmetro url é criado a partir do array item, que, por sua vez, recebe dados controlados pelo usuário. Esses dados chegam como entrada no corpo da mensagem, na variável req.body.content.
Corrigindo a injeção de comandos
Agora, podemos tomar outras medidas para resolver o problema de segurança, como:
Em vez de usar a função insegura
exec(), podemos usar a versão segura dessa API:execFile(), que escapa os argumentos fornecidos na forma de um argumento de função do tipo array.Podemos — e devemos — validar, escapar ou higienizar a variável item recebida do usuário antes que ela chegue a um código sensível, como o que executa processos do sistema.
Resumo
Parabéns por ter chegado até aqui!
Esperamos que agora você entenda melhor os problemas que as vulnerabilidades de injeção de código podem causar, sejam elas originadas no seu próprio código ou em dependências de terceiros importadas para a aplicação.
Se esta publicação foi útil, confira também estas leituras recomendadas pelos meus colegas da Snyk:
Se você ou sua equipe desenvolvem em Go, confira este guia de segurança para Go: 8 boas práticas de segurança para desenvolvedores
Se você trabalha com Java e, em particular, com Spring MVC, esta leitura pode ser interessante: Como resolver problemas de segurança em uma aplicação Spring MVC com Java, que também identifica problemas de segurança com o Snyk Code
Comece a jogar Capture the Flag
Aprenda a resolver desafios de Capture the Flag assistindo à gravação sob demanda do nosso workshop virtual introdutório.
