Démonstration de pollution de prototype JavaScript
Section intitulée « Démonstration de pollution de prototype JavaScript »La pollution de prototype JavaScript apparaît lorsqu’une fusion récursive traite des clés contrôlées par un attaquant comme des propriétés ordinaires. Une seule entrée JSON peut alors modifier le comportement d’objets sans lien apparent, jusqu’au déni de service ou à l’exécution de code selon les gadgets présents.
Une application peut ranger des informations dans des casiers partagés. Si une personne extérieure réussit à modifier la règle commune de tous les casiers, des décisions prises plus tard peuvent changer sans que personne ne touche directement aux dossiers concernés. Ce défaut sert surtout à comprendre pourquoi accepter des données n’est jamais les recopier aveuglément.
→ Lire l’explication complète de la marmotte
Prototype JavaScript et héritage implicite
Section intitulée « Prototype JavaScript et héritage implicite »Un objet JavaScript ordinaire possède un lien interne vers un autre objet appelé son prototype. Lorsqu’une propriété est demandée et absente de l’objet courant, le moteur la cherche dans ce prototype, puis dans la chaîne de prototypes. Pour {}, cette chaîne aboutit généralement à Object.prototype.
const compte = {};
console.log(compte.role); // undefinedObject.prototype.role = "admin";console.log(compte.role); // "admin", propriété héritéeObject.prototype est donc une surface globale pour les objets ordinaires du même processus. Modifier cet objet n’ajoute pas une valeur visible dans chaque objet existant, mais change le résultat de leurs recherches de propriétés. Le moteur ne distingue pas une propriété héritée malveillante d’une propriété héritée légitime. Il applique sa règle. Froide, déterministe, et parfaitement indifférente au fait qu’une revue de code ait validé trois slides sur le zero trust.
La clé spéciale __proto__ expose historiquement ce lien de prototype. Affecter cible.__proto__ peut modifier le prototype de cible. Une fusion récursive vulnérable combine deux erreurs : elle accepte une clé réservée et elle descend dans une propriété héritée au lieu de créer une propriété propre.
Cette classe n’existe pas telle quelle dans les langages sans héritage prototypal dynamique. Un dictionnaire y reste généralement un conteneur de paires clé-valeur. JavaScript mélange objets métier, dictionnaires génériques et mécanisme d’héritage dans la même syntaxe. C’est expressif. C’est aussi le genre de confort qui facture son supplément au pire moment.
Le compromis central est net : la fusion profonde générique accepte des structures évolutives avec peu de code, mais elle abandonne le contrôle sur la sémantique des clés. Pour toute donnée issue d’une requête, d’un fichier importé ou d’un message inter-services, il faut préférer un mapping explicite vers un modèle connu. La fusion profonde d’entrée hostile doit être considérée comme interdite, pas comme une commodité à encadrer vaguement.
Laboratoire Node.js : fusion récursive vulnérable
Section intitulée « Laboratoire Node.js : fusion récursive vulnérable »Le laboratoire nécessite Node.js 20 ou supérieur et aucun paquet externe. Cette absence de dépendance est volontaire : ajouter une bibliothèque pour démontrer une primitive cassée reviendrait à installer une alarme incendie sur un grille-pain déjà en feu.
Créer server.js, puis lancer le processus dans un répertoire isolé :
const http = require("node:http");
// Vulnérable : __proto__ atteint Object.prototype via une propriété héritée.function merge(target, source) { for (const key in source) { if (source[key] !== null && typeof source[key] === "object") { target[key] ??= {}; merge(target[key], source[key]); } else { target[key] = source[key]; } } return target;}
http.createServer((request, response) => { if (request.method !== "POST" || request.url !== "/profile") { response.writeHead(404).end(); return; }
let body = ""; request.on("data", chunk => { body += chunk; }); request.on("end", () => { try { const input = JSON.parse(body); const profile = merge({}, input);
// Gadget applicatif : propriété héritée traitée comme une autorisation. const result = { profile, isAdmin: profile.isAdmin === true }; response.writeHead(200, { "content-type": "application/json" }); response.end(JSON.stringify(result)); } catch { response.writeHead(400).end("JSON invalide"); } });}).listen(3000, () => console.log("http://127.0.0.1:3000"));
// Terminal 1 : node server.js// Terminal 2 :// curl -sS -X POST http://127.0.0.1:3000/profile \// -H 'content-type: application/json' \// --data '{"__proto__":{"isAdmin":true}}'La réponse contient "isAdmin":true, alors que le JSON envoyé ne contient aucune propriété isAdmin au niveau du profil. JSON.parse crée bien une propriété propre nommée __proto__, ce qui évite les ambiguïtés de littéraux JavaScript. Le problème arrive ensuite.
Dans merge, target vaut initialement {}. Pour la clé __proto__, l’expression target[key] lit Object.prototype, car cette propriété est héritée. L’opérateur ??= ne crée donc rien, puisque la valeur lue n’est ni null ni undefined. L’appel récursif devient merge(Object.prototype, { isAdmin: true }). La propriété isAdmin est alors écrite dans le prototype global.
Le processus reste pollué après la requête. Toute requête suivante peut hériter de isAdmin, même avec un corps parfaitement inoffensif. Un test automatisé qui réutilise le même processus sans nettoyage peut donc contaminer les tests suivants. Les résultats deviennent créatifs, ce qui est un mot poli pour dire inutilisables.
Gadgets, déni de service et exécution de code
Section intitulée « Gadgets, déni de service et exécution de code »La pollution seule ne produit pas automatiquement une exécution de code à distance, ou RCE (Remote Code Execution). Elle installe une valeur dans une position globale ; un gadget doit ensuite la consommer dangereusement. Le gadget est un chemin applicatif qui interprète une propriété héritée comme un paramètre de sécurité, d’exécution ou de contrôle de flux.
Les impacts fréquents sont les suivants :
- contournement d’autorisation avec
isAdmin,role,enabledoucanExport; - déni de service, ou DoS (Denial of Service), par pollution de paramètres utilisés dans des boucles, des limites de taille ou des options de sérialisation ;
- altération d’options passées à un moteur de modèles, un chargeur de modules ou un processus enfant ;
- exécution de code si une propriété polluée active un interpréteur, un shell ou une fonction de compilation dynamique.
Un gadget child_process.exec avec une option shell héritée, ou un moteur de rendu qui honore une option héritée de compilation, change une pollution en incident majeur. Mais annoncer une RCE sans identifier ce consommateur est du théâtre de vulnérabilité. L’impact doit être démontré depuis la clé injectée jusqu’au point d’usage.
La surface d’attaque dépasse les routes HTTP. Les analyseurs de paramètres imbriqués, fichiers YAML ou JSON, webhooks, messages de file, formulaires d’administration et chargements de configuration sont concernés dès qu’ils alimentent une fusion. Une passerelle applicative ne corrige rien si l’application accepte ensuite la structure toxique. Filtrer au bordure sans invariant local, c’est mettre un panneau « ne pas entrer » devant une porte qui reste ouverte.
Une revue doit rechercher merge, extend, defaultsDeep, les copies récursives maison et tout accès à des options par objet.cle sans contrôle de propriété propre. Object.hasOwn(objet, "cle") est préférable à objet.hasOwnProperty, car cette dernière méthode peut être absente ou écrasée.
Validation des clés et objets sans prototype
Section intitulée « Validation des clés et objets sans prototype »La contre-mesure principale est une liste blanche de clés par type de donnée. Une API de profil n’a pas besoin d’accepter une structure arbitraire : elle accepte displayName, locale et quelques champs documentés, puis construit un nouvel objet. Ce modèle élimine à la fois __proto__, prototype, constructor et les futurs noms dangereux non encore connus.
function parseProfile(input) { const allowed = new Set(["displayName", "locale"]); const profile = Object.create(null);
for (const [key, value] of Object.entries(input)) { if (!allowed.has(key)) { throw new Error(`Clé interdite: ${key}`); } if (typeof value !== "string" || value.length > 80) { throw new Error(`Valeur invalide: ${key}`); } profile[key] = value; }
return profile;}Bloquer uniquement __proto__ est insuffisant. Le chemin constructor.prototype peut atteindre le prototype global dans plusieurs fonctions de fusion. La liste noire doit donc couvrir au minimum __proto__, prototype et constructor, mais elle reste un filet, pas une politique de données. Une liste blanche liée au schéma métier est la décision à retenir.
Object.create(null) crée un objet sans prototype. Il convient aux dictionnaires sensibles, aux accumulations de paramètres et aux tables d’index. Il empêche la lecture héritée qui déclenche le laboratoire. En revanche, cet objet n’a ni toString, ni hasOwnProperty, ni les méthodes habituelles. Ce coût est sain pour une structure de données, moins pour un objet métier manipulé par une bibliothèque qui suppose Object.prototype.
Mettre à jour les bibliothèques de fusion reste obligatoire. Consulter les avis de sécurité, verrouiller les versions et générer un inventaire des composants logiciels, ou SBOM (Software Bill of Materials), permet d’identifier les versions exposées. Une version corrigée réduit les vecteurs connus, mais ne rend pas sûre une fonction maison qui mélange des objets hostiles. Les failles applicatives se corrigent rarement par une ligne de dépendance seule.
Exploitation, supervision et reprise
Section intitulée « Exploitation, supervision et reprise »Cette vulnérabilité n’implique pas de clé cryptographique à faire tourner. Les actifs sensibles sont les données qui transitent dans les structures fusionnées : jetons de session, drapeaux d’autorisation, options d’exécution et secrets de configuration. Ils ne doivent jamais être fusionnés avec une entrée non fiable. Le reste doit provenir de sources séparées et validées, avec les règles de gestion des secrets décrites côté développement.
Journaliser les rejets de clés interdites avec la route, le type de message et un identifiant de corrélation. Ne pas journaliser le corps complet lorsqu’il peut contenir des mots de passe, des jetons ou des données personnelles. Une hausse de rejets __proto__, constructor ou prototype signale soit un scan, soit une intégration défectueuse. Les deux méritent investigation.
La reprise exige le redémarrage de tous les workers contaminés, car la pollution vit en mémoire de processus. Retirer la requête des journaux ou corriger le code sans redémarrer ne restaure pas Object.prototype. Les sauvegardes de configuration doivent être validées avant restauration : restaurer un document contenant des clés interdites réintroduit le défaut avec le cérémonial habituel des incidents évitables.
Ajouter des tests négatifs exécutés dans un processus neuf : envoyer chaque clé réservée, vérifier le rejet, puis vérifier qu’un objet nouvellement créé ne possède aucune propriété inattendue. Ce contrôle complète les revues d’architecture décrites dans l’architecture cybersécurité. Il ne remplace pas le mapping explicite. Rien ne remplace le fait de ne pas donner à une entrée hostile le pouvoir de redéfinir les règles du langage.
🐝 Ce qui arrive si on s’en passe
Section intitulée « 🐝 Ce qui arrive si on s’en passe »Sans validation explicite des clés entrantes, une seule requête suffit à modifier durablement le comportement d’un processus, et cette modification ne laisse aucune trace persistante : rien en base, rien sur disque, seulement un état mémoire qui disparaît au redémarrage. L’analyse post incident perd donc son preuve principale. Derrière un répartiteur de charge, seuls certains workers sont touchés, ce qui produit des réponses divergentes pour une même requête et fait classer l’anomalie en défaut intermittent plutôt qu’en intrusion. Les bibliothèques tierces du même processus héritent aussi des propriétés injectées, y compris celles qui n’ont jamais lu l’entrée hostile, ce qui étend l’impact au delà de la route vulnérable. Sans clé interdite rejetée, aucun compteur ne monte, et la tentative reste indistinguable d’un appel légitime.
🦫 Explication de la marmotte 🍫
Section intitulée « 🦫 Explication de la marmotte 🍫 »JavaScript permet de créer des objets qui peuvent chercher une propriété dans un objet commun quand ils ne la possèdent pas eux-mêmes. Par exemple, si un profil ne contient pas de droit particulier, le programme peut regarder dans cet objet commun avant de répondre qu’il n’existe pas. Ce comportement est prévu par le langage.
Le problème commence lorsqu’un programme reçoit des données extérieures et les copie récursivement dans un objet existant. Certaines clés ont une signification spéciale. La clé __proto__ peut orienter le programme vers l’objet commun au lieu de créer une donnée ordinaire. Si la fonction de copie ne contrôle pas les clés, elle écrit alors des propriétés fournies par l’attaquant dans cet objet commun.
Après cette écriture, des objets créés plus tard peuvent sembler posséder une propriété qu’ils n’ont jamais reçue. Une vérification de droit peut alors accepter un compte. Une limite de taille peut disparaître. Une option dangereuse peut être activée si une autre partie du programme lit cette propriété sans vérifier son origine.
La correction consiste à accepter uniquement les noms de champs attendus, à vérifier leur type et leur taille, puis à créer un nouvel objet. Les structures servant de dictionnaires peuvent aussi être créées sans objet commun. Enfin, il faut redémarrer les programmes déjà contaminés, car la modification reste présente tant que le processus fonctionne.
À lire aussi
- Désérialisation non sécurisée et chaîne de gadgetsFabriquer un objet sérialisé malveillant qui déclenche l'exécution de code à la désérialisation, le démontrer en laboratoire, puis refermer la faille par liste blanche de classes et formats de données inertes.
- Pollution de variable d'environnement héritée dans un conteneurDémonstration en laboratoire du détournement du comportement d'un processus enfant via une variable d'environnement injectable dans un conteneur, avec le mécanisme d'héritage et les contre-mesures d'assainissement.
- Images Docker de base et confiance héritéeGérer la généalogie des images de base Docker pour maîtriser la confiance, les vulnérabilités et l'exploitation.
- JavaScript et SQL côté web, ce qu'il faut savoir des deux langagesCe qu'il faut connaître du modèle d'exécution de JavaScript et de SQL pour lire un code web, relire une revue de sécurité ou interpréter un rapport de scan.
Pages voisines
La citent
- Cross-site scripting (XSS) : mécanisme, variantes et défenses
- Comment valider le function calling d’un LLM
- JavaScript et SQL côté web, ce qu'il faut savoir des deux langages
Démonstration de pollution de prototype JavaScript
Elle cite
