Skip to main content

Les risques de sécurité d’un sandbox JavaScript utilisant le module VM de Node.js

Écrit par
feature red team blue team

22 février 2023

0 minutes de lecture

Vous devez créer un produit qui nécessite d’exécuter du code JavaScript dynamique fourni par les utilisateurs ? Vous pensez peut-être que le module VM de Node.js est une solution viable pour créer un sandbox JavaScript. Dans cet article, nous verrons pourquoi cette approche est loin d’être recommandée et quelles en sont les implications en matière de sécurité.

De temps à autre, un projet vient bousculer les tâches rudimentaires et routinières du développement backend. Des API ? Des files de messages ? Des traitements lourds et des besoins importants en calcul ? Non. Voici une tâche du backlog à prendre en considération :

As a user,
I want to write and execute my own custom JavaScript code,
So that it can run within the platform and provide me feedback.

Cet exemple est un peu vague, car il s’agit d’un cas d’usage générique portant sur l’exécution de code non fiable fourni par des utilisateurs. Il existe toutefois des exemples concrets où cela peut s’avérer nécessaire, notamment :

  • Le produit replit vous permet de coder dans le cloud et d’exécuter du code dans un IDE personnalisé.

  • Diverses plateformes d’entraînement au code et d’entretien technique, comme « leet code », vous permettent d’écrire du code personnalisé, par exemple pour implémenter un algorithme donné ou rédiger des tests.

Voici le module VM de Node.js.

Le module VM de Node.js

Le module central node:vm permet aux développeurs de compiler et d’exécuter du code fourni dynamiquement dans des contextes de machine virtuelle V8. Cela peut d’abord vous faire penser à la fonction JavaScript eval, qui présente un risque de sécurité bien connu. Mais le module VM de Node.js est-il plus sûr ? Après tout, il s’agit d’une « machine virtuelle ».

Voyons un exemple concret :

const vm = require("node:vm");

const productExpirationDays = 14;

// This works just fine - it is a custom code that manipulates
// the data in the context and only there.
const userInputCustomJavaScriptCode = "userCustomNickname = 'Johnny Mnemonic';";

const context = { userCustomNickname: "John Nash" };
vm.createContext(context);

vm.runInContext(userInputCustomJavaScriptCode, context);

console.log(context.userCustomNickname);
console.log(context.productExpirationDays);
console.log(productExpirationDays);

Dans l’extrait de code ci-dessus, le code JavaScript personnalisé fourni par l’utilisateur (indiqué par la variable userInputCustomJavaScriptCode) est exécuté spécifiquement dans un contexte donné de variables, à l’aide de vm.runInContext(). Cet extrait s’exécute correctement et produit les résultats suivants :

Johnny Mnemonic
undefined
14

Les attaquants peuvent toutefois chercher à exploiter ce type de code JavaScript dynamique en modifiant d’autres variables que celles définies à l’origine. Dans cet exemple fictif en Node.js, une variable nommée productExpirationDays définit la durée de validité pour l’utilisateur. Que se passerait-il si un attaquant modifiait son code JavaScript dynamique pour augmenter cette valeur ?

const userInputCustomJavaScriptCode = "userCustomNickname = 'Johnny Mnemonic'; productExpirationMinutes += 60";

Si nous remplacions l’exemple précédent de saisie utilisateur par celui-ci, qui tente d’augmenter le nombre de jours de validité, nous obtiendrions l’erreur suivante :

evalmachine.<anonymous>:1
userCustomNickname = 'Johnny Mnemonic'; productExpirationDays += 60
                                        ^

ReferenceError: productExpirationDays is not defined
    at evalmachine.<anonymous>:1:41
    at Script.runInContext (node:vm:141:12)
    at Object.runInContext (node:vm:297:6)

Cette erreur s’explique par le fait que la variable productExpirationDays n’est pas définie ni présente dans la portée du contexte de la machine virtuelle où le code JavaScript dynamique est exécuté.

En somme, il semble que nous ayons trouvé un moyen d’exécuter du code JavaScript dynamique de façon sécurisée dans un sandbox JavaScript isolé.

Un sandbox JavaScript non sécurisé

L’exemple de code utilisant createContext() et vm.runInContext() du module VM de Node.js était trop simpliste. Malheureusement, dans la réalité, les entrées malveillantes des utilisateurs recourent souvent à des méthodes plus astucieuses, créatives et efficaces pour s’échapper du sandbox JavaScript.

Voyons comment un attaquant peut fournir du code non sécurisé entraînant une attaque par déni de service contre une application en cours d’exécution. Prenons le script Node.js suivant :

const vm = require("node:vm");

const userInputCustomJavaScriptCode =
  "userCustomNickname = 'Johnny Mnemonic'; while(true) {}";

const context = { userCustomNickname: "John Nash" };
vm.createContext(context);

vm.runInContext(userInputCustomJavaScriptCode, context);

// This will never run:
console.log("Never fear, I is here");

Dans l’exemple de code ci-dessus, l’utilisateur a ajouté une boucle infinie, while(true) {}, à sa saisie. Même si ce code ne modifie aucune autre variable de l’application, il provoque une attaque par déni de service.

Les risques liés à un sandbox JavaScript non sécurisé concernent également l’exécution de code à distance. Avec this.constructor.constructor, nous pouvons accéder à l’objet JavaScript Function. Une fonction JavaScript peut accepter du code sous forme de chaîne, puis l’exécuter. Notre attaque exploite cette caractéristique et, à l’aide d’une fonction immédiatement invoquée (IIFE), affiche les variables d’environnement du processus Node.js en cours d’exécution :

const vm = require("node:vm");

const userInputCustomJavaScriptCode =
  "this.constructor.constructor('console.log(process.env)')()";

const context = { userCustomNickname: "John Nash" };
vm.createContext(context);

vm.runInContext(userInputCustomJavaScriptCode, context);

console.log("Mess with the best, drop like your envs!");

L’exécution de l’extrait de code ci-dessus affiche le résultat de process.env, qui répertorie toutes les variables d’environnement.

Vous devriez désormais mesurer l’impact d’une exécution de code à distance lorsqu’elle permet d’exécuter du code non fiable dans le module VM de Node.js. Si les utilisateurs peuvent exécuter du code personnalisé, ils ont un accès complet à l’environnement d’exécution du serveur Node.js et peuvent lancer des processus, accéder au système de fichiers et bien plus encore.

En résumé

Dans un environnement Node.js, un sandbox JavaScript non sécurisé peut avoir de graves conséquences et paralyser toute une application. Nous avons vu comment un attaquant peut exploiter le sandbox JavaScript et fournir des entrées malveillantes qui dégradent une application Node.js, provoquant ainsi une vulnérabilité de déni de service. Mais la surface d’attaque ne s’arrête pas là. Nous avons également vu qu’une exécution de code à distance est possible, permettant aux attaquants d’exécuter du code personnalisé dans des environnements serveur Node.js et de mettre en péril toute la plateforme applicative.

En réalité, la faille de sécurité créée par un sandbox JavaScript utilisant le module VM de Node.js n’est que le début d’une violation de données plus vaste. Dès qu’un seul serveur d’application Node.js est compromis, des informations sensibles peuvent être révélées, comme les secrets d’accès à une base de données ou à des services cloud. Un attaquant peut alors poursuivre son exploitation et se déplacer latéralement au sein du réseau déployé.

La bonne pratique consiste donc à ne pas s’appuyer sur le module VM de Node.js comme sandbox sécurisé pour exécuter du code JavaScript non fiable. La documentation de l’API Node.js le précise clairement : « Le module node:vm n’est pas un mécanisme de sécurité. Ne l’utilisez pas pour exécuter du code non fiable. »

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.