Skip to main content

5 façons d’empêcher l’injection de code en JavaScript et Node.js

Écrit par
Prevent code injection vulnerabilities with Snyk

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 :

  1. Évitez eval(), setTimeout() et setInterval()

  2. Évitez new Function()

  3. Évitez la sérialisation du code en JavaScript

  4. Utilisez un linter de sécurité pour Node.js

  5. 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 :

const getElementForm = getElementType == “id” ? “getElementById” : “getElementByName”;
const priceTagValue = eval(“document.”+getElementForm+”(“+elementId+”).value”);

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 :

const db = "./db.json"
const dataPoints = eval("require('"+db+"')");

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 :

Diff de code dans lib/dust.js : mise à jour de dust.escapeHtml pour traiter les valeurs assimilables à des chaînes avant leur échappement HTML

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 :

Éditeur de code affichant un modèle Dust avec une logique conditionnelle pour différentes tailles de texte dans le corps du contenu sur ordinateur et mobile

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 :

Éditeur de code en thème sombre affichant du code d’assistance Dust.js avec une instruction eval et des fichiers de projet dans la barre latérale

Tout devient alors clair : plusieurs problèmes de sécurité se combinent de façon imprévisible :

  1. 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.

  2. 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 :

Liran Tal - Stranger Danger: Finding Security Vulnerabilities Before They Find You!

É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 :

setTimeout(“console.log(1+1)”, 1000);

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 :

const addition = new Function(‘a’, ‘b’, ‘return a+b’);
addition(1, 1)

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 :

Tableau de bord Snyk Advisor présentant le package npm js-yaml, avec un score de santé de 88/100 et des indicateurs de popularité, de maintenance, de sécurité et de communauté.

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() :

function resolveJavascriptFunction(object /*, explicit*/) {
  /*jslint evil:true*/  var func;

  try {
    func = new Function('return ' + object);
    return func();
  } catch (error) {
    return NIL;
  }
}

Voyons à quoi pourrait ressembler une preuve de concept exploitant cette vulnérabilité :

var yaml = require('js-yaml');

x = "test: !!js/function > \n \
function f() { \n \
console.log(1); \n \
}();"

yaml.load(x);

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 :

"plugins": [
  "security"
],
"extends": [
  "plugin:security/recommended"

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 :

  1. 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. Mais matchEmailRegEx est peut-être simplement une constante dans mon fichier shared/variables.js. Le linter n’est pas assez avancé pour le savoir.

  2. 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 que someCommand est 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.

  3. 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 :

Tableau de bord des projets affichant le menu « Ajouter un projet », où GitHub est mis en évidence parmi les intégrations de dépôts disponibles.

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 :

Écran de sélection des dépôts GitHub filtré sur « goof », avec le dépôt « goof » sélectionné.

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 :

Tableau de bord d’analyse du code du dépôt lirantal/goof, affichant le nombre de problèmes détectés dans le code, le Dockerfile et package.json.

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

Tableau de bord d’analyse du code mettant en évidence une vulnérabilité d’injection de commandes dans du code JavaScript, avec l’appel exec vulnérable et les détails de la solution.

Les problèmes de sécurité relevés sur cette ligne de code expliquent le risque :

“Unsanitized input from the HTTP request body flows into child_process.exec, where it is used to build a shell command. This may result in a Command Injection vulnerability.”

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 :

Analyse du flux de données d’une injection de commande montrant qu’une entrée HTTP non assainie atteint un appel exec mis en évidence dans routes/index.js.

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 :

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.