5 façons d’empêcher l’injection de code en JavaScript et Node.js
6 avril 2021
0 minutes de lectureÉcrire du code JavaScript sécurisé de façon à empêcher l’injection de code peut sembler une tâche ordinaire, mais les pièges sont nombreux. Par exemple, le fait que vous, en tant que développeur, suiviez les bonnes pratiques de sécurité ne signifie pas que les autres en font autant. Vous utilisez probablement des packages open source dans votre application. Comment savoir s’ils ont été développés de manière sécurisée ? Et si du code non sécurisé comme eval() s’y trouvait ? Voyons cela de plus près.
Qu’est-ce que l’injection de code ?
L’injection de code est une forme particulière des attaques par injection, plus générales, dans lesquelles un attaquant peut envoyer du code JavaScript ou Node.js interprété par le navigateur ou l’environnement d’exécution Node.js. La vulnérabilité de sécurité apparaît lorsque l’interpréteur ne parvient pas à distinguer le code fiable prévu par le développeur du code injecté par l’attaquant en entrée.
Comment empêcher l’injection de code
Parmi les principes clés de la programmation sécurisée, il faut éviter toute exécution dynamique de code dans l’application. Cela signifie qu’il vaut mieux éviter les constructions du langage comme eval, ainsi que les chaînes de code transmises à setTimeout() ou au constructeur Function. Ensuite, évitez la sérialisation, qui peut être vulnérable aux attaques par injection exécutant du code pendant le processus de sérialisation. Enfin, analysez les dépendances pour vous assurer que votre application n’est pas exposée à cette attaque via des composants open source tiers. De plus, avec un outil d’analyse statique du code comme Snyk Code, vous pouvez détecter ces vulnérabilités potentielles d’injection de code dans votre code ou celui de vos collègues.
Dans cet article, nous allons examiner 5 façons d’empêcher l’injection de code :
Évitez
eval(),setTimeout()etsetInterval()Évitez
new Function()Évitez la sérialisation du code en JavaScript
Utilisez un linter de sécurité pour Node.js
Utilisez un outil d’analyse statique du code (SCA) pour détecter et corriger les problèmes d’injection de code
1. Évitez eval(), setTimeout() et setInterval()
Je sais ce que vous pensez : encore un guide qui me dit d’éviter eval. C’est vrai, mais je veux aussi vous montrer des exemples concrets d’autres bibliothèques populaires qui ont utilisé eval (ou d’autres formes de construction de code) et qui en ont ensuite subi les conséquences, jusqu’à provoquer une grave vulnérabilité de sécurité.
Avant d’examiner les références à des packages tiers vulnérables, commençons par expliquer eval et les fonctions associées. Les environnements d’exécution JavaScript, comme les navigateurs et la plateforme côté serveur Node.js, permettent d’évaluer et d’exécuter du code à l’exécution. En voici un exemple pratique :
Ici, le programmeur cherche à créer un moyen dynamique d’accéder aux données du DOM. Dans cet exemple, on suppose que getElementform peut aussi être contrôlé par l’utilisateur, tout comme la variable elementId. Il existe de meilleures façons d’effectuer cette tâche sans recourir à eval ; vous devriez donc éviter à tout prix le code dynamique de ce type.
Côté Node.js, on peut vouloir autoriser l’accès à certains points de données de l’application en fonction d’une évaluation dynamique. Voici un exemple :
Dans cet exemple, on suppose généralement que le fichier exact à charger avec require est déterminé dynamiquement et potentiellement contrôlé par l’utilisateur. Cela crée donc un risque de vulnérabilité de sécurité par injection de code.
L’injection de code dans Dustjs illustre concrètement les risques liés à l’utilisation non sécurisée d’eval
Le package npm dustjs de LinkedIn, un projet de templates asynchrones pour le navigateur et Node.js côté serveur, montre à quel point une vulnérabilité d’injection de code peut être grave.
Bien que ce package ne soit plus vraiment maintenu, il totalise encore environ 72 000 téléchargements par mois et a dû faire face à une vulnérabilité de sécurité par injection de code.
Les responsables de la maintenance de dustjs ont fait de leur mieux pour échapper les entrées utilisateur potentiellement dangereuses susceptibles d’être transmises à des constructions de code non sécurisées comme la fonction eval(). Cependant, la fonction escapeHtml présentait elle-même une faille de sécurité : elle vérifiait uniquement que les données étaient des chaînes de caractères avant de les échapper, alors qu’elle aurait également dû vérifier d’autres types, comme les tableaux. Cette pull request a corrigé la vulnérabilité de sécurité par injection de code :

Vous vous demandez comment eval() intervient dans tout cela ?
Si vous utilisez dustjs, vous pouvez aussi ajouter le package npm dustjs-helpers pour bénéficier de fonctions d’aide supplémentaires dans les templates, comme des opérations mathématiques et logiques. L’une de ces fonctions est une condition if, que vous pourriez utiliser ainsi dans vos propres fichiers de template dust :

Logique, n’est-ce pas ?
Le problème, c’est que les entrées utilisateur non contrôlées du paramètre de requête device sont directement transmises à la fonction d’aide de la condition if, qui utilise eval, comme vous pouvez le voir à la ligne 227, pour évaluer la condition de façon dynamique :

Tout devient alors clair : plusieurs problèmes de sécurité se combinent de façon imprévisible :
dustjs-linkedin, un package open source, présente une faille de sécurité : les chaînes d’entrée ne sont pas correctement assainies dans sa fonction
escapeHtml.dustjs-helpers, un package open source, utilise une pratique de codage non sécurisée, à savoir la fonction
eval(), pour évaluer dynamiquement du code à l’exécution.
Vous voulez voir comment j’ai exploité cette vulnérabilité et piraté une véritable application en fonctionnement en m’appuyant précisément sur cette faille ? Découvrez-le ici :

Évitez aussi setTimeout() et setInterval()
Pour conclure sur la bonne pratique qui consiste à éviter eval(), je veux également attirer votre attention sur d’autres fonctions que vous connaissez certainement ou que vous avez déjà utilisées dans votre application en tant que développeur JavaScript : setTimeout() et setInterval().
On sait moins que ces fonctions acceptent aussi des chaînes de code. Voici par exemple comment les utiliser :
Heureusement, les littéraux de chaîne ne sont pas autorisés dans un environnement Node.js !
2. Évitez new Function()
Une autre construction du langage, similaire aux fonctions eval(), setTimeout() et setInterval() présentées plus haut, est le constructeur Function, qui permet de définir dynamiquement une fonction à partir de littéraux de chaîne.
Prenons un exemple tout simple :
Si vous avez suivi jusque-là, vous connaissez déjà les problèmes de sécurité potentiels que peut entraîner la transmission d’entrées utilisateur à une telle fonction…
3. Évitez la sérialisation du code en JavaScript
La sérialisation est très courante dans l’écosystème Java. Mon ami Brian Vermeer a écrit un article sur l’impact des vulnérabilités de sécurité sur les applications Java, dû à des opérations de sérialisation non sécurisées. Je vous le recommande vivement : Sérialisation et désérialisation en Java : comprendre la vulnérabilité de désérialisation Java.
Revenons à l’univers JavaScript : la sérialisation y est aussi très présente.
Il y a de fortes chances que vous n’écriviez pas vous-même votre logique de sérialisation et de désérialisation. Mais dans le merveilleux univers de npm, avec plus de 1 500 000 packages open source à votre disposition, pourquoi s’en priver ?
js-yaml est très populaire, avec plus de 28 000 000 de téléchargements par semaine. Selon Snyk Advisor, son état général est bon :

Cela dit, comme le montre la capture d’écran ci-dessus du package npm js-yaml, les versions précédentes présentaient des vulnérabilités de sécurité. Lesquelles, vous demandez-vous ?
Certaines versions de js-yaml étaient vulnérables à l’exécution de code due à la désérialisation. Cette vulnérabilité se manifeste notamment par l’utilisation suivante du constructeur new Function() :
Voyons à quoi pourrait ressembler une preuve de concept exploitant cette vulnérabilité :
Si un acteur malveillant parvient à fournir cette entrée, ou une partie de celle-ci, telle qu’elle est utilisée pour créer la variable x dans le code de preuve de concept ci-dessus, une vulnérabilité potentielle devient alors une véritable menace.
La vulnérabilité ci-dessus remonte à 2013, mais un rapport de sécurité de 2019 a révélé un autre cas d’exécution de code arbitraire dans js-yaml. Soyez donc prudent ou, pour vous donner un conseil plus concret et applicable : évitez new Function() et analysez vos packages open source tiers afin de vous assurer qu’ils ne présentent pas ces vulnérabilités. Si c’est le cas, vous pourrez les corriger automatiquement à l’aide d’une pull request de correction.
4. Utilisez un linter de sécurité pour Node.js
Passons aux outils de ce guide et parlons des linters. Les développeurs JavaScript les apprécient. Que vous utilisiez standardjs ou eslint pour appliquer un style de code, ces outils sont très courants dans les projets JavaScript et Node.js.
Pourquoi ne pas appliquer aussi de bonnes pratiques de sécurité ? C’est là qu’intervient eslint-plugin-security. Comme l’indiquent les instructions du fichier README, l’utilisation du plugin est très simple. Il suffit d’ajouter la configuration suivante du plugin eslint pour activer la configuration recommandée :
En quoi le linter est-il utile ?
Il comprend des règles permettant de détecter les pratiques de codage non sécurisées, par exemple detect-eval-with-expression, qui repère les utilisations de eval() avec des expressions ou des littéraux de chaîne. Le linter propose aussi d’autres règles, notamment pour l’utilisation des API Node.js child_process.
Notez que la dernière publication d’eslint-plugin-security remonte à plus de 4 ans. Même s’il fonctionne peut-être encore bien, vous pouvez envisager d’autres packages qui lui succèdent, comme eslint-plugin-security-node.
5. Utilisez un outil d’analyse statique du code pour détecter et corriger les problèmes d’injection de code
Les linters d’analyse statique du code (SCA), dans leur forme élémentaire telle qu’utilisée avec ESLint, constituent un bon point de départ. Ils fournissent suffisamment de contexte pour faire respecter le style de code, mais comme nous l’avons vu avec le linter de sécurité Node.js, ils manquent de flexibilité pour traiter efficacement les problèmes de sécurité.
Parmi les difficultés que les développeurs rencontrent avec un linter de sécurité Node.js comme eslint-plugin-security, on peut citer :
Les faux positifs : Les règles du linter sont assez élémentaires et peuvent générer de nombreux faux positifs, ce qui ne fait qu’accroître la frustration et la confusion des développeurs. Par exemple, l’expression
RegExp(matchEmailRegEx)suivante provoquera une erreur du linter de sécurité Node.js, car la fonction RegExp n’est pas utilisée avec un littéral. MaismatchEmailRegExest peut-être simplement une constante dans mon fichier shared/variables.js. Le linter n’est pas assez avancé pour le savoir.Des règles trop rigides : Pour reprendre le point précédent, la règle est trop stricte. Soit vous utilisez
child_process.exec(someCommand, []), soit vous ne l’utilisez pas. Le processus d’analyse statique du code utilisé par le linter n’est pas assez intelligent pour déterminer quesomeCommandest une constante que vous avez définie en dur. Le simple fait d’utiliser child_process.exec() avec une valeur non littérale déclenche une erreur du linter, ce qui finit par frustrer les développeurs, qui désactivent alors la règle.Des règles trop élémentaires : L’ensemble de règles est trop limité et les résultats trop simplistes. C’est essentiellement tout ou rien, sans beaucoup de contexte sur le cheminement des données, depuis une entrée utilisateur donnée jusqu’à du code potentiellement sensible, comme l’exécution de commandes, les requêtes SQL ou d’autres opérations.
Pour reprendre ce que je disais plus haut, un linter de sécurité comme eslint-plugin-security-node ou un outil équivalent est un bon point de départ. C’est toujours mieux que rien.
Mais il existe de meilleures façons de détecter les problèmes de sécurité dans votre propre code, pendant que vous codez.
Laissez-moi vous présenter Snyk Code, un outil de test de sécurité des applications statique (SAST) conçu pour les développeurs.
Détecter une injection de commande dans une application Node.js
Snyk Code sera bientôt disponible, mais je vais vous donner un aperçu de son fonctionnement.
Tout d’abord, connectez-vous à Snyk avec un compte GitHub, puis importez le dépôt GitHub. Pour cela, cliquez sur Add project, puis sur l’icône GitHub :

Ensuite, choisissez un dépôt dans la liste ou saisissez son nom dans la barre de recherche, puis activez-le pour lancer l’analyse :

Snyk importera alors le dépôt GitHub et l’analysera rapidement.
Snyk détectera automatiquement les autres fichiers manifestes susceptibles de révéler des problèmes de sécurité, par exemple si vous utilisez des dépendances open source présentant des vulnérabilités connues ou si votre image Docker introduit elle aussi de nombreuses vulnérabilités.
Concentrons-nous sur notre propre code dans cette application Node.js. Cliquons sur Analyse du code pour voir ce que nous découvrons :

Snyk Code a détecté plusieurs vulnérabilités. En voici une : une vulnérabilité d’injection de commandes.

Les problèmes de sécurité relevés sur cette ligne de code expliquent le risque :
Mais comment les données passent-elles du paramètre url à la fonction exec() non sécurisée ? Cliquez sur le bouton Afficher tous les détails pour obtenir une représentation plus complète du flux de données et mieux comprendre le contexte :

Nous pouvons ici voir clairement l’ensemble du processus tel que Snyk Code l’a analysé.
Le paramètre url est créé à partir du tableau item, qui est lui-même alimenté par une entrée contrôlée par l’utilisateur et transmise dans le corps du message via la variable req.body.content.
Corriger l’injection de commandes
Nous pouvons maintenant prendre d’autres mesures pour résoudre ce problème de sécurité, par exemple :
Au lieu d’utiliser la fonction non sécurisée
exec(), nous pouvons utiliser la version sécurisée de cette API :execFile(). Celle-ci échappe les arguments qui lui sont transmis sous la forme d’un tableau.Nous pouvons et devons valider, échapper ou assainir la variable item provenant des entrées utilisateur avant qu’elle n’atteigne du code sensible, par exemple une fonction qui exécute des processus système.
En résumé
Bravo si vous êtes arrivé jusqu’ici !
Vous comprenez maintenant mieux les problèmes que peuvent entraîner les vulnérabilités d’injection de code, qu’elles proviennent de votre propre code ou de dépendances tierces que vous intégrez à votre application.
Si cet article vous a été utile, voici quelques lectures complémentaires de mes collègues chez Snyk :
Si vous ou votre équipe développez en Go, consultez ce antisèche de sécurité Go : 8 bonnes pratiques de sécurité pour les développeurs Go
Si vous développez en Java, et plus particulièrement avec Spring MVC, cet article pourrait vous intéresser : Résoudre les problèmes de sécurité Java dans mon application Spring MVC, qui détecte également les problèmes de sécurité avec Snyk Code
Lancez-vous dans les compétitions Capture The Flag
Apprenez à résoudre des défis Capture The Flag en regardant à la demande notre atelier virtuel d’initiation.
