Après la faille de runtime de Node2Shell et la chaîne détournée de la prototype pollution, voici la voie la plus directe vers l'exécution de code : la lui donner. L'injection de code côté serveur, c'est le moment où une entrée utilisateur cesse d'être une donnée et devient une instruction. En Node.js, le piège a une variante particulièrement répandue : croire que le module vm protège, alors qu'il n'a jamais été conçu pour cela. Cet article décrit le mécanisme avec le code, démonte le faux bac à sable, et montre comment isoler vraiment quand c'est inévitable.
Quand une donnée devient du code
Certaines primitives de JavaScript prennent une chaîne de caractères et l'exécutent comme du code. eval est la plus connue, mais new Function, le Function constructor, et setTimeout/setInterval appelés avec une chaîne font la même chose :
// Toutes ces formes exécutent la chaîne reçue
eval(entreeUtilisateur);
new Function(entreeUtilisateur)();
Function(entreeUtilisateur)();
setTimeout(entreeUtilisateur, 0); // la chaîne est évaluée comme du code
setInterval(entreeUtilisateur, 1000);Dès qu'une de ces primitives reçoit une entrée que l'attaquant contrôle, même partiellement, le serveur exécute le code de l'attaquant dans son propre processus, avec les droits de ce processus. C'est la classe CWE-94. Il n'y a pas de détournement subtil comme en injection SQL : l'exécution est immédiate et totale.
// Cas réel typique : un "moteur de règles" configurable
app.post("/calcul", (req, res) => {
const formule = req.body.formule; // "prix * quantite"
const resultat = eval(formule); // ... ou "require('child_process').execSync('id')"
res.json({ resultat });
});Le cas se cache souvent derrière une bonne intention : un champ « formule » dans un tableur applicatif, un moteur de règles « configurable », une fonctionnalité qui laisse l'utilisateur écrire une petite expression. Chaque fois que cette expression est confiée au moteur JavaScript complet, la porte est ouverte.
Le faux bac à sable : le module vm
Devant ce risque, le réflexe est de chercher un bac à sable, et le module vm de Node.js en a l'apparence : il exécute du code dans un contexte séparé. Beaucoup en concluent qu'il isole. C'est faux, et la documentation de Node.js le dit sans détour : le module vm n'est pas un mécanisme de sécurité et ne doit pas servir à exécuter du code non fiable.
La raison est structurelle. Le code lancé dans un contexte vm tourne dans le même processus, et il peut en sortir. Le chemin d'évasion classique remonte la chaîne de prototypes d'un objet partagé jusqu'à son constructor, puis au Function constructor, pour reconstruire un accès au contexte global de l'hôte :
const vm = require("node:vm");
// Ce qu'un développeur croit être un bac à sable :
const bacASable = { entree: userData };
vm.createContext(bacASable);
// Charge typique d'évasion exécutée DANS le contexte vm :
const evasion = `
this.constructor.constructor("return process")()
.mainModule.require("child_process")
.execSync("id").toString()
`;
vm.runInContext(evasion, bacASable); // atteint process, puis le systèmethis.constructor donne le constructeur d'un objet du contexte ; .constructor à nouveau donne le Function constructor ; l'appeler reconstruit une fonction qui s'exécute dans la portée de l'hôte, d'où l'accès à process et à require. Les bibliothèques qui promettaient un vrai isolement, vm2 en tête, ont connu des évasions à répétition, jusqu'à ce que leur maintenance cesse faute de pouvoir fermer la classe entière. La leçon est nette : il n'existe pas de bac à sable fiable en pur JavaScript dans le même processus.
Isoler vraiment, quand c'est inévitable
La bonne réponse est presque toujours : ne pas exécuter de code utilisateur du tout. Quand une fonctionnalité semble l'exiger, on remet d'abord en cause le besoin, car il cache souvent une mauvaise conception. Souvent, un interpréteur à grammaire restreinte suffit et supprime le risque :
// Pour évaluer une expression arithmétique utilisateur, PAS le moteur JS complet
import { evaluate } from "mathjs"; // grammaire limitée, pas d'accès à require/process
const resultat = evaluate("prix * quantite", { prix: 10, quantite: 3 });Quand l'exécution de vrai code est réellement nécessaire, l'isolement doit être au niveau du système, pas du langage :
- un processus séparé sans privilèges, avec des limites mémoire et CPU imposées par l'OS ;
- un conteneur jetable, sans accès réseau ni montage sensible ;
- un isolat dédié (
isolated-vm, ou les isolats des runtimes edge) qui crée une vraie frontière V8.
// Si isolation en processus : isolated-vm, avec limites explicites
import ivm from "isolated-vm";
const isolate = new ivm.Isolate({ memoryLimit: 16 }); // Mo
const context = await isolate.createContext();
// Exécuter avec un timeout, sans jamais exposer de référence à l'hôte
await context.eval(codeUtilisateur, { timeout: 100 });L'isolement vaut par la frontière, jamais par une barrière interne au même processus qui partage le même tas.
Se protéger, en synthèse
- Ne jamais passer une entrée non fiable à
eval,new Function, auFunctionconstructor, ni àsetTimeout/setIntervalsous forme de chaîne. - Ne pas considérer le module
vmcomme une barrière de sécurité : ce n'en est pas une, de l'aveu même de Node.js. - Remettre en cause le besoin d'exécuter du code utilisateur avant de chercher à le sécuriser.
- Pour évaluer une expression légitime, utiliser un interpréteur à grammaire restreinte, jamais le moteur JavaScript complet.
- Quand l'exécution est inévitable, isoler au niveau du système (processus sans privilèges, conteneur jetable, isolat dédié), avec limites mémoire, CPU et réseau.
- Valider les entrées en amont reste la première ligne : voir les bonnes pratiques de validation d'entrées.
La règle tient en une phrase : aucune donnée issue d'une requête ne doit devenir du code. C'est le principe le plus simple de cette série sur les voies vers la RCE en Node.js, et le plus souvent enfreint par confort.
S'entraîner sur un lab réel
L'injection de code se saisit mieux en voyant une évasion de vm fonctionner sous ses propres yeux. Le parcours PHP → shell de Zeroday Cyber Academy applique cette pédagogie côté PHP : exploiter pour comprendre, puis défendre, sur des cibles réelles avec remédiation avant/après.
Sources
- Node.js, documentation du module
vm(avertissement de sécurité explicite) : https://nodejs.org/api/vm.html - MITRE, CWE-94 « Improper Control of Generation of Code (Code Injection) » : https://cwe.mitre.org/data/definitions/94.html
- OWASP, « Code Injection » : https://owasp.org/www-community/attacks/Code_Injection



