Vulnérabilités des extensions complémentaires C/C++ de NodeJS
Alessio Della Libera
14 août 2024
0 minutes de lectureL’un des principaux objectifs de cette recherche était d’étudier les vulnérabilités C/C++ dans le contexte des packages npm NodeJS. Nous nous sommes concentrés sur l’exploration et l’identification de vulnérabilités classiques, telles que les dépassements de tampon, les dénis de service (plantage du processus, types non vérifiés) et les fuites de mémoire, dans le contexte des modules complémentaires C/C++ NodeJS, ainsi que sur la modélisation des sources, des puits et des assainisseurs pertinents avec Snyk Code (voir Snyk apporte une approche AppSec pensée pour les développeurs au C/C++).
Cette recherche porte sur les packages NPM qui utilisent des interfaces C/C++ dans leur implémentation. Nous n’avons pas ciblé les projets qui ne sont pas répertoriés sur NPM.
Dans cet article, nous présentons les vulnérabilités courantes et les modèles vulnérables susceptibles d’apparaître lors de l’écriture d’extensions complémentaires C/C++ pour NodeJS. Nous proposons également des exemples de correctifs et des recommandations à l’intention des responsables de projets open source.
Cet article s’inspire de l’étude « Bilingual Problems: Studying the Security Risks Incurred by Native Extensions in Scripting Languages » de Cristian-Alexandru Staicu, Sazzadur Rahaman, Àgnes Kiss et Michael Backes.[1] Dans leur étude originale, les auteurs analysent les risques de sécurité liés aux extensions natives dans des langages populaires, dont JavaScript.
Présentation des modules complémentaires C/C++ de NodeJS
NodeJS propose différentes API pour appeler du code natif C/C++. Cette recherche porte sur les vulnérabilités de sécurité susceptibles de survenir lors de l’utilisation de l’un des mécanismes suivants :
node_api.h: Node-APInapi.h: Wrapper C++ autour de Node-API
Vous trouverez des exemples d’utilisation des bibliothèques ci-dessus sur GitHub.
Pour une présentation complète des modules complémentaires et de leur compilation, consultez la documentation officielle de NodeJS.
Les vulnérabilités présentées et identifiées dans au moins un package sont les suivantes :
Fuites de mémoire
Type non vérifié (DoS)
Assertion accessible (DoS)
Exceptions non gérées (DoS)
Dépassement de tampon
Dépassement d’entier
Dans les sections suivantes, nous présentons des exemples de modèles vulnérables et expliquons les conditions à réunir pour que la vulnérabilité puisse être exploitée.
Exemples de modèles vulnérables
Dans cette section, nous allons voir comment les API propres aux modules complémentaires peuvent entraîner des problèmes de sécurité si elles ne sont pas gérées correctement, ainsi que certains modèles vulnérables identifiés dans le cadre de cette étude.
REMARQUE : les exemples suivants ne constituent pas une liste exhaustive. D’autres cas de figure peuvent entraîner des problèmes de sécurité
qui ne sont pas abordés dans cet article.
Configuration
Installez node-gyp (https://github.com/nodejs/node-gyp).
Les fichiers suivants servent à exécuter les exemples de la section suivante :
package.json
binding.gyp
Exécutez les commandes suivantes pour compiler les extensions C/C++ :
node-gyp configurenode-gyp build
Exécuter un exemple spécifique :
main.js
Exceptions non gérées
Impact : déni de service (DoS)
napi
L’API napi propose différentes fonctions pour gérer les exceptions et lever des erreurs. Toutefois, selon le drapeau utilisé dans le fichier binding.gyp, certaines précautions s’imposent pour éviter les plantages inattendus.
Par exemple, si le drapeau NAPI_DISABLE_CPP_EXCEPTIONS est défini dans le fichier binding.gyp, les situations suivantes peuvent entraîner le plantage du processus (DoS) :
Napi::TypeError::New(env, "").ThrowAsJavaScriptException();ainsi que d’autres fonctions susceptibles de générer une erreur (par exemple, un argument de type incorrect)throw Napi::Error::Newsans bloctry/catchPlusieurs appels à
Napi::TypeError::New(env, "").ThrowAsJavaScriptException();sansreturn, susceptibles d’être exécutés dans une même fonction
Comme l’explique la documentation, « après avoir levé une exception JavaScript, le code doit généralement retourner immédiatement de la fonction de rappel native, après avoir effectué tout nettoyage nécessaire. » .
test_napi_exceptions.cpp
Exécutez ces exemples :
Assertion accessible
Impact : déni de service (DoS)
node_api
En examinant les exemples fournis, on constate que certains exemples utilisent assert pour vérifier la valeur de retour de certaines fonctions. Toutefois, si une valeur contaminée (provenant du code JavaScript) atteint un assert pendant l’exécution du programme, cela peut provoquer un plantage (DoS). En examinant certains projets, nous avons trouvé plusieurs assertions accessibles dans la logique du code. J’ai donc jugé utile de les mentionner dans la liste précédente.
Pour corriger ce problème, vous pouvez vérifier la valeur de retour dans un if, puis renvoyer la valeur appropriée (selon la logique du programme), au lieu d’utiliser assert.
test_node_api_assert.c
Exécutez cet exemple :
Type de données non vérifié
Impact : déni de service (DoS)
napi
napi propose plusieurs API pour convertir les types JavaScript. Par exemple,
Napi::Value::ToString() « renvoie la valeur Napi::Value convertie en chaîne JavaScript ». De même, Napi::Value::ToNumber() « renvoie la valeur Napi::Value convertie en nombre JavaScript ».
En interne, l’API Napi::Value::ToString() de napi appelle napi_coerce_to_string de Node-API :
De même, l’API Napi::Value::ToNumber() de napi appelle en interne napi_coerce_to_number de Node-API :
Selon la documentation officielle de napi_coerce_to_string : « Cette API implémente l’opération abstraite ToString() définie à la section 7.1.13 de la spécification du langage ECMAScript. Cette fonction peut exécuter du code JS si la valeur transmise est un objet. » Cela signifie que si l’entrée utilisateur définit une propriété toString, la valeur de cette propriété sera renvoyée (au lieu d’appeler toString()), ce qui peut produire des résultats inattendus.
Si nous appelons d’autres méthodes sur les valeurs renvoyées par Napi::Value::ToString() et que l’entrée définit une propriété toString, une exception peut se produire et entraîner, le plus souvent, le plantage du processus. Il en va de même pour napi_coerce_to_number.
Modèle vulnérable :
appels tels que
Napi::String::Utf8Value()sur uneNapi::Valueobtenue à partir deToString()ouToNumber, sans vérification adéquate du type
Pour éviter ces situations, vérifiez que la valeur renvoyée par Napi::Value::ToString() ou Napi::Value::ToNumber() est respectivement une chaîne ou un nombre avant d’appeler d’autres méthodes sur cette valeur.
REMARQUE : comme pour les cas d’exceptions non gérées mentionnés précédemment, ces problèmes surviennent si le drapeau NAPI_DISABLE_CPP_EXCEPTIONS est défini dans le fichier binding.gyp.
test_napi_unchecked_type.cpp
Exécutez ces exemples :
Fuites de mémoire
Impact : divulgation d’informations
napi
L’API napi propose plusieurs méthodes pour créer une valeur de chaîne JavaScript à partir d’une chaîne C encodée en UTF-8, UTF-16LE ou ISO-8859-1. Ces API sont :
Toutes ces méthodes ont la même signature :
La valeur à vérifier attentivement est [in] length, c’est-à-dire la longueur de la chaîne en octets. Si cette valeur est contrôlée par un attaquant ou codée en dur alors que la valeur d’entrée est contaminée, des valeurs mémoire inattendues peuvent être stockées dans result.
Pour éviter ce type de problème, utilisez NAPI_AUTO_LENGTH pour la valeur size_t length.
Modèle vulnérable :
napi_create_string_*avec une valeursize_t lengthsupérieure à la longueur deconst char* str
test_napi_memory_leak.c
Exécutez cet exemple :
Méthodologie
Pour tester et détecter automatiquement autant de problèmes que possible, j’ai utilisé l’approche suivante afin de tirer parti des capacités de Snyk Code :
Créer un ensemble de données de packages npm qui font appel au C/C++ via les API des modules complémentaires NodeJS
Écrire des règles de sécurité dans Snyk Code pour modéliser :
Sources : dans ce contexte, les sources sont des valeurs provenant du code JavaScript, qui peuvent être issues de
Napi::CallbackInfo::Env()dans le contexte denapi– ou denapi_get_value_*– dans le contexte denode_apiPuits : selon le problème de sécurité, j’ai modélisé la présence de plusieurs appels à
ThrowAsJavaScriptExceptiondans une même fonction, la vérificationassertet plusieurs méthodes utilisées pour créer des valeurs de chaîne, entre autres. J’ai également pris en compte les situations où le code n’est pas vulnérable grâce à la présence de certains arguments, commeNAPI_AUTO_LENGTHdans le cas des fuites de mémoire.
Écrire des règles qui utilisent les sources et les puits définis pour effectuer une analyse de contamination et suivre la contamination des sources aux puits
Utiliser les sources définies dans les règles existantes prises en charge (par exemple, Dépassement de tampon ou Dépassement d’entier), afin de couvrir encore plus de vulnérabilités C/C++ (pas seulement celles propres aux API des modules complémentaires NodeJS)
Exécuter ces règles sur l’ensemble de données créé précédemment
Examiner manuellement les résultats et, le cas échéant, créer une preuve de concept (PoC)
Grâce à cette approche, j’ai pu détecter plusieurs problèmes dans des packages npm en modélisant les API pertinentes des modules complémentaires NodeJS avec Snyk Code.
Toutefois, pour certains des problèmes détectés, j’ai prélevé un échantillon de projets dans l’ensemble de données créé et les ai examinés manuellement.
Résultats
Cette recherche a permis de détecter plusieurs vulnérabilités dans des packages. Vous les trouverez ci-dessous :
Conclusion
À titre personnel, cette recherche a été une expérience d’apprentissage incroyable pour plusieurs raisons. J’ai eu l’occasion d’explorer en profondeur l’univers des modules complémentaires NodeJS, d’étudier les travaux existants sur les problèmes déjà recensés et de tenter de modéliser certains scénarios avec Snyk Code pour détecter des problèmes dans un vaste ensemble de dépôts.
Même si je connais assez bien JavaScript et plusieurs autres langages, j’ai commencé à apprendre le C/C++ récemment, dans le cadre de notre travail visant à prendre en charge plusieurs règles de sécurité désormais accessibles aux clients de Snyk Code. En associant ces deux aspects — l’apprentissage et la possibilité d’utiliser Snyk Code pour modéliser plusieurs problèmes de sécurité —, j’ai beaucoup apprécié cette recherche. Je tiens donc à remercier Snyk de m’avoir offert cette occasion.
Références
Node-API - https://nodejs.org/api/n-api.html
node-addon-api - https://github.com/nodejs/node-addon-api
Modules complémentaires C++ - https://nodejs.org/api/addons.html


