Vulnerabilidades em extensões de add-ons C/C++ do NodeJS
Alessio Della Libera
14 de agosto de 2024
0 minutos de leituraUm dos principais objetivos desta pesquisa foi explorar vulnerabilidades em C/C++ no contexto de pacotes npm do NodeJS. O foco será explorar e identificar vulnerabilidades clássicas, como estouro de buffer, negação de serviço (falha do processo, tipos não verificados) e vazamentos de memória em add-ons C/C++ do NodeJS, além de modelar fontes, destinos e sanitizadores relevantes usando o Snyk Code (veja Snyk leva uma abordagem de AppSec que prioriza desenvolvedores para C/C++).
Os alvos desta pesquisa são pacotes NPM que usam interfaces C/C++ em sua implementação. Não incluímos projetos que não estão listados no NPM.
Nesta publicação, apresentamos uma visão geral das vulnerabilidades de segurança e dos padrões vulneráveis mais comuns que podem surgir ao escrever add-ons C/C++ para NodeJS. Também apresentamos exemplos de correção e sugestões para mantenedores de código aberto.
Esta publicação foi inspirada no artigo “Bilingual Problems: Studying the Security Risks Incurred by Native Extensions in Scripting Languages”, de Cristian-Alexandru Staicu, Sazzadur Rahaman, Àgnes Kiss e Michael Backes.[1] No artigo original, os autores analisaram os riscos de segurança de extensões nativas em linguagens populares, incluindo JavaScript.
Contexto dos add-ons C/C++ do NodeJS
O NodeJS oferece diferentes APIs para chamar código nativo C/C++. O escopo desta pesquisa é investigar vulnerabilidades de segurança que podem ocorrer ao usar um dos seguintes mecanismos:
node_api.h: Node-APInapi.h: Wrapper C++ para Node-API
Um bom recurso com exemplos de uso das bibliotecas acima está disponível no GitHub.
Para uma introdução completa aos add-ons e instruções para compilá-los, consulte a documentação oficial do NodeJS.
As vulnerabilidades analisadas e identificadas em pelo menos um pacote são:
Vazamentos de memória
Tipo não verificado (DoS)
Assert alcançável (DoS)
Exceções não tratadas (DoS)
Estouro de buffer
Estouro de inteiro
Nas próximas seções, apresentaremos exemplos de padrões vulneráveis e explicaremos as condições necessárias para que a vulnerabilidade possa ser explorada.
Exemplos de padrões vulneráveis
Nesta seção, vamos explorar como APIs específicas de add-ons podem causar problemas de segurança quando não são tratadas corretamente, além de alguns padrões vulneráveis identificados durante este estudo.
OBSERVAÇÃO: os exemplos a seguir não representam uma lista completa. Pode haver outros cenários
que levem a problemas de segurança não abordados nesta publicação.
Configuração
Instale o node-gyp (https://github.com/nodejs/node-gyp).
Os arquivos a seguir são usados para executar os exemplos na próxima seção:
package.json
binding.gyp
Execute os comandos a seguir para compilar as extensões C/C++:
node-gyp configurenode-gyp build
Executar um exemplo específico:
main.js
Exceções não tratadas
Impacto: negação de serviço (DoS)
napi
A API napi oferece diferentes funções para tratar exceções e lançar erros. No entanto, dependendo da flag usada no arquivo binding.gyp, é preciso tomar alguns cuidados para evitar falhas inesperadas.
Por exemplo, se a flag NAPI_DISABLE_CPP_EXCEPTIONS estiver definida no arquivo binding.gyp, os cenários a seguir podem causar uma falha do processo (DoS):
Napi::TypeError::New(env, "").ThrowAsJavaScriptException();, além de outras funções que podem gerar um erro (por exemplo, um argumento do tipo incorreto)throw Napi::Error::Newsem estar dentro de um blocotry/catchVárias chamadas a
Napi::TypeError::New(env, "").ThrowAsJavaScriptException();semreturn, que podem ser alcançadas dentro da mesma função
Como explicado na documentação, “depois de lançar uma exceção JavaScript, o código geralmente deve retornar imediatamente do callback nativo, após realizar qualquer limpeza necessária.” .
test_napi_exceptions.cpp
Execute estes exemplos:
Assert alcançável
Impacto: negação de serviço (DoS)
node_api
Ao analisar os exemplos fornecidos, vemos que em alguns exemplos, assert é usado para verificar o valor de retorno de algumas funções. No entanto, se um assert for alcançado por valores contaminados (provenientes do código JavaScript) durante a execução do programa, isso pode causar uma falha (DoS). Ao analisar alguns projetos, encontramos várias ocorrências de asserts alcançáveis na lógica do código, então achei importante mencioná-las na lista acima.
Uma possível correção para esse cenário é verificar o valor de retorno dentro de um if e, em seguida, retornar o valor apropriado (de acordo com a lógica do programa), em vez de usar um assert.
test_node_api_assert.c
Execute este exemplo:
Tipo de dado não verificado
Impacto: negação de serviço (DoS)
napi
napi oferece várias APIs para converter tipos JavaScript. Por exemplo,
Napi::Value::ToString() “retorna o Napi::Value convertido em uma string JavaScript.” Da mesma forma, Napi::Value::ToNumber() “retorna o Napi::Value convertido em um número JavaScript.”
Nos bastidores, a API Napi::Value::ToString() de napi chama napi_coerce_to_string da Node-API:
Da mesma forma, nos bastidores, a API Napi::Value::ToNumber() de napi chama napi_coerce_to_number da Node-API:
Na documentação oficial de napi_coerce_to_string: “Esta API implementa a operação abstrata ToString(), definida na seção 7.1.13 da especificação da linguagem ECMAScript. Esta função pode executar código JS se o valor passado for um objeto.” Isso significa que, se a entrada do usuário definir uma propriedade toString, o valor dessa propriedade será retornado (em vez de chamar toString()), o que pode gerar resultados inesperados.
Se chamarmos outros métodos nos valores retornados por Napi::Value::ToString() e a entrada definir uma propriedade toString, poderá ocorrer uma exceção, que na maioria das vezes causa uma falha do processo. O mesmo vale para napi_coerce_to_number.
Padrão vulnerável:
chamadas como
Napi::String::Utf8Value()em umNapi::Valueresultante deToString()ouToNumber, sem a verificação adequada do tipo
Para evitar esses cenários, uma possível correção é verificar se o valor retornado por Napi::Value::ToString() ou Napi::Value::ToNumber() é, respectivamente, uma string ou um número, antes de chamar outros métodos nesses valores.
OBSERVAÇÃO: assim como nos casos de exceções não tratadas mencionados anteriormente, esses problemas ocorrem quando a flag NAPI_DISABLE_CPP_EXCEPTIONS está definida no arquivo binding.gyp.
test_napi_unchecked_type.cpp
Execute estes exemplos:
Vazamentos de memória
Impacto: divulgação de informações
napi
A API napi oferece vários métodos para criar um valor de string JavaScript a partir de uma string C codificada em UTF8, UTF16-LE ou ISO-8859-1. São eles:
Todos esses métodos têm a mesma assinatura:
O valor que merece atenção especial é [in] length, ou seja, o comprimento da string em bytes. Se esse valor for controlado por um invasor ou estiver definido no código e o valor de entrada for contaminado, valores inesperados da memória poderão ser armazenados em result.
Para evitar esse tipo de problema, use NAPI_AUTO_LENGTH para o valor size_t length.
Padrão vulnerável:
napi_create_string_*comsize_t lengthmaior que o comprimento deconst char* str
test_napi_memory_leak.c
Execute este exemplo:
Metodologia
Para testar e encontrar automaticamente o maior número possível de problemas, usei a seguinte abordagem para aproveitar o potencial do Snyk Code:
Criar um conjunto de dados de pacotes npm que chamam C/C++ usando APIs de add-ons do NodeJS
Escrever regras de segurança no Snyk Code para modelar:
Fontes: neste contexto, fontes são valores provenientes do código JavaScript, ou seja, dados que podem vir de
Napi::CallbackInfo::Env()no contexto denapiou denapi_get_value_*no contexto denode_apiDestinos: dependendo do problema de segurança, modelei a presença de várias chamadas a
ThrowAsJavaScriptExceptiondentro da mesma função, a verificaçãoasserte vários métodos usados para criar valores de string (para citar alguns). Também levei em conta situações em que o código não é vulnerável devido à presença de determinados argumentos, comoNAPI_AUTO_LENGTHem casos de vazamento de memória
Escrever regras que usem os destinos e as fontes definidos para realizar uma análise de taint e rastrear a contaminação das fontes até os destinos
Usar as fontes definidas nas regras existentes que oferecemos (por exemplo, Estouro de buffer ou Estouro de inteiro) para abranger ainda mais vulnerabilidades em C/C++ (não apenas as específicas de APIs de add-ons do NodeJS)
Executar essas regras no conjunto de dados criado anteriormente
Analisar os resultados manualmente e, se necessário, criar uma PoC
Com essa abordagem, consegui encontrar vários problemas em pacotes npm ao modelar as APIs relevantes dos add-ons do NodeJS usando o Snyk Code.
No entanto, para alguns dos problemas encontrados, selecionei alguns projetos do conjunto de dados criado e os analisei manualmente.
Resultados
Esta pesquisa identificou várias vulnerabilidades em pacotes. Confira a seguir:
Conclusão
Em termos pessoais, esta pesquisa foi uma experiência de aprendizado incrível por vários motivos. Tive a oportunidade de me aprofundar no universo dos add-ons do NodeJS, revisar a literatura existente sobre problemas conhecidos e tentar modelar alguns cenários usando o Snyk Code para encontrar problemas em um grande conjunto de repositórios.
Embora eu já conheça bem JavaScript e muitas outras linguagens, comecei a aprender C/C++ recentemente, por causa do trabalho que fizemos (e continuamos fazendo) para oferecer suporte a várias regras de segurança que agora estão disponíveis para clientes do Snyk Code. Ao unir os dois aspectos — a experiência de aprendizado e a oportunidade de usar o Snyk Code para modelar vários problemas de segurança —, gostei muito desta pesquisa e quero agradecer à Snyk pela oportunidade.
Referências
Node-API - https://nodejs.org/api/n-api.html
node-addon-api - https://github.com/nodejs/node-addon-api
Add-ons C++ - https://nodejs.org/api/addons.html


