Pollution de Prototype : La vulnérabilité JavaScript qui empoisonne votre application ☣️

Dans le domaine de la sécurité des applications web, certaines vulnérabilités se manifestent par des crashs spectaculaires ou des violations évidentes. D’autres agissent silencieusement, corrompant votre application dès ses fondations. La pollution de prototype appartient à cette seconde catégorie, une vulnérabilité subtile mais dévastatrice qui exploite l’héritage prototypal de JavaScript pour empoisonner chaque objet de votre application avec une seule entrée malveillante.
Comprendre la chaîne de prototypes en JavaScript
Avant d’aborder l’attaque, il est essentiel de comprendre comment fonctionne l’héritage basé sur les prototypes en JavaScript. Contrairement aux langages orientés objet classiques, JavaScript utilise des prototypes pour partager propriétés et méthodes entre objets. Chaque objet JavaScript possède un lien interne vers un autre objet appelé son prototype, formant ce qu’on appelle la chaîne de prototypes.
Lorsque vous accédez à une propriété sur un objet, JavaScript vérifie d’abord si cette propriété existe directement sur l’objet. Si ce n’est pas le cas, il remonte la chaîne de prototypes, vérifiant chaque prototype jusqu’à ce qu’il trouve la propriété ou atteigne la fin de la chaîne à Object.prototype. Ce mécanisme élégant permet un partage efficace des propriétés mais crée aussi une surface d’attaque dangereuse.
Considérez ce code apparemment innocent :
const user = { name: 'Alice' };
console.log(user.toString); // [Function: toString]
Même si nous n’avons jamais défini de propriété toString sur l’objet user, JavaScript la trouve en parcourant la chaîne de prototypes jusqu’à Object.prototype, où toString est défini. Ce comportement est fondamental en JavaScript, mais il devient dangereux lorsque des attaquants peuvent manipuler ces prototypes.
Qu’est-ce que la pollution de prototype ?
La pollution de prototype est une vulnérabilité qui permet aux attaquants d’injecter des propriétés dans les prototypes des constructions du langage JavaScript, exploitant le fait que JavaScript autorise la modification de tous les attributs d’objet, y compris des propriétés spéciales comme __proto__, constructor, et prototype.
L’attaque fonctionne en exploitant la gestion de l’affectation de propriétés d’objet en JavaScript. Lorsqu’une application fusionne des données contrôlables par l’utilisateur dans des objets existants sans validation appropriée, un attaquant peut créer une entrée malveillante qui modifie le Object.prototype de base. Étant donné que presque tous les objets en JavaScript héritent de Object.prototype, cette modification unique affecte toute l’application.
Ces vulnérabilités apparaissent généralement lorsque des fonctions JavaScript fusionnent récursivement des objets contenant des propriétés contrôlables par l’utilisateur dans des objets existants sans d’abord nettoyer les clés, notamment lors du traitement de données JSON provenant de sources non fiables.
L’anatomie d’une attaque de pollution de prototype
Examinons comment fonctionne une attaque réelle de pollution de prototype en utilisant une fonction de fusion vulnérable, similaire à celles trouvées dans des bibliothèques populaires. Voici un exemple simplifié :
function merge(target, source) {
for (let key in source) {
if (typeof source[key] === 'object') {
if (!target[key]) target[key] = {};
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// Usage normal
const user = {};
merge(user, { name: 'Alice', role: 'user' });
console.log(user); // { name: 'Alice', role: 'user' }
Cette fonction semble inoffensive. Elle fusionne récursivement les propriétés d’un objet source dans un objet cible. Cependant, regardez ce qui se passe lorsqu’un attaquant fournit une entrée malveillante :
// Charge utile malveillante
const maliciousInput = JSON.parse('{"__proto__": {"isAdmin": true}}');
// Pollution du prototype
merge({}, maliciousInput);
// Maintenant, TOUS les objets ont la propriété isAdmin
const normalUser = {};
console.log(normalUser.isAdmin); // true - pollution réussie !
L’attaquant a exploité la propriété spéciale __proto__ pour atteindre Object.prototype et injecter une propriété isAdmin. Désormais, chaque objet dans l’application, y compris ceux nouvellement créés qui n’ont jamais été proches de notre fonction de fusion, hérite de cette propriété empoisonnée.
Impact réel : étude de cas Lodash
La bibliothèque populaire Lodash a été affectée par des vulnérabilités de pollution de prototype dans des fonctions comme defaultsDeep, merge, et mergeWith, qui permettent à des utilisateurs malveillants de modifier le prototype de Object via __proto__, pouvant affecter des millions d’applications dans le monde.
Considérez ce code vulnérable utilisant la fonction merge de Lodash :
const _ = require('lodash');
const express = require('express');
const app = express();
app.use(express.json());
app.post('/api/update-settings', (req, res) => {
const userSettings = {};
// Vulnérable : fusion de données non fiables
_.merge(userSettings, req.body);
// Plus tard dans l'application
if (userSettings.isAdmin) {
// Accès admin
return res.json({ access: 'admin' });
}
res.json({ access: 'user' });
});
Un attaquant pourrait envoyer cette charge utile :
{
"__proto__": {
"isAdmin": true
}
}
Cette requête unique empoisonne le prototype, et soudain, chaque objet dans l’application, y compris ceux utilisés pour les vérifications d’autorisation, hérite de la propriété isAdmin définie sur true. L’impact se propage à toute l’application.
De la pollution à l’exécution de code à distance
Les conséquences de la pollution de prototype vont bien au-delà d’une simple escalade de privilèges. En environnement Node.js, des attaquants expérimentés peuvent enchaîner la pollution de prototype avec d’autres vulnérabilités pour réaliser une exécution de code à distance (RCE). Cela se produit via une technique appelée “gadget chains”, où des propriétés polluées interagissent avec des chemins de code existants de manière inattendue.
Par exemple, si une application utilise la création de processus enfants et se fie à des propriétés héritées pour la construction de commandes, un attaquant pourrait injecter des commandes malveillantes via pollution de prototype :
// Code vulnérable
const { spawn } = require('child_process');
function executeCommand(options) {
const defaultOptions = {};
// Options peuvent hériter de propriétés polluées
const finalOptions = Object.assign(defaultOptions, options);
spawn('node', [finalOptions.script || 'default.js'], {
shell: finalOptions.shell || false,
env: finalOptions.env || process.env
});
}
Si un attaquant pollue le prototype avec {"shell": true, "script": "; malicious-command"}, il pourrait obtenir une exécution de code lorsque cette fonction s’exécute avec des vérifications de propriété insuffisantes.
Cross-Site Scripting via pollution de prototype
Les attaquants peuvent polluer les prototypes avec des propriétés comme innerHTML, src, ou onerror, et si l’application référence ensuite ces propriétés et les insère dans le DOM, cela devient une vulnérabilité XSS. Cette variante est particulièrement dangereuse dans les frameworks JavaScript côté client :
// Rendu de template vulnérable
function renderContent(element, data) {
element.innerHTML = data.content || 'Contenu par défaut';
}
// L'attaquant pollue le prototype
const malicious = JSON.parse('{"__proto__": {"content": "<img src=x onerror=alert(document.cookie)>"}}');
merge({}, malicious);
// Plus tard dans le code
const emptyData = {};
renderContent(document.getElementById('output'), emptyData);
// XSS déclenché via prototype pollué!
Détection dans le monde réel
Des chercheurs utilisant des techniques avancées de fuzzing ont découvert 65 nouvelles vulnérabilités de pollution de prototype dans des scénarios zero-day qui ne pouvaient pas être détectés par des méthodes traditionnelles, soulignant la persistance de ce problème. La subtilité de la pollution de prototype est un défi. Contrairement à l’injection SQL ou XSS, qui produisent souvent des erreurs immédiates ou des effets visibles, la pollution de prototype peut rester silencieuse dans votre code, attendant les bonnes conditions pour causer une défaillance catastrophique.
Programmation défensive : construire des applications résistantes à la pollution
Protéger votre application contre la pollution de prototype nécessite une approche multicouche combinant bonnes pratiques de codage, validation d’entrée et décisions architecturales.
Utiliser Object.create(null) pour les dictionnaires
Créer des objets sans prototypes, avec Object.create(null), brise la chaîne de prototypes et empêche la pollution. C’est l’une des défenses les plus efficaces :
// Sécurisé : pas de chaîne de prototype
const userSettings = Object.create(null);
userSettings.name = 'Alice';
// Impossible à polluer
const malicious = JSON.parse('{"__proto__": {"isAdmin": true}}');
Object.assign(userSettings, malicious);
console.log(userSettings.isAdmin); // undefined - protégé !
Les objets créés de cette manière n’ont pas de prototype, ce qui les rend immunisés contre la pollution de prototype. Utilisez ce pattern pour tout objet contenant des données contrôlables par l’utilisateur.
Valider et assainir les propriétés
Validez et assainissez toujours les clés d’objet avant de traiter les entrées utilisateur :
function secureMerge(target, source) {
const dangerousKeys = ['__proto__', 'constructor', 'prototype'];
for (let key in source) {
// Bloquer les clés dangereuses
if (dangerousKeys.includes(key)) {
continue;
}
// Validation supplémentaire
if (typeof key !== 'string' || key.startsWith('_')) {
continue;
}
if (typeof source[key] === 'object' && source[key] !== null) {
if (!target[key]) target[key] = Object.create(null);
secureMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
Utiliser Object.freeze() pour les objets critiques
Geler les prototypes et objets critiques pour empêcher leur modification :
// Empêcher la pollution de prototype au niveau racine
Object.freeze(Object.prototype);
Object.freeze(Object);
// Geler les objets de configuration
const config = Object.freeze({
apiUrl: 'https://api.example.com',
timeout: 5000
});
Bien que cette approche soit efficace, faites attention car elle peut casser du code légitime qui dépend de modifications de prototype. Utilisez-la de manière sélective pour des objets sensibles à la sécurité.
Préférer Map aux objets simples
En pratique recommandée, utilisez Map plutôt qu’un objet pour stocker des paires clé-valeur, car les Maps n’héritent pas de Object.prototype :
// Alternative sécurisée aux objets
const userSettings = new Map();
userSettings.set('name', 'Alice');
userSettings.set('role', 'user');
// Non vulnérable à la pollution de prototype
console.log(userSettings.get('isAdmin')); // undefined
Les Maps offrent une API plus propre pour le stockage clé-valeur et éliminent totalement les risques de pollution de prototype.
Valider le schéma
Utilisez des bibliothèques de validation de schéma pour faire respecter des structures d’objets strictes :
const Joi = require('joi');
const userSchema = Joi.object({
name: Joi.string().required(),
email: Joi.string().email().required(),
role: Joi.string().valid('user', 'admin').required()
}).unknown(false); // Rejette les propriétés inconnues
app.post('/api/users', (req, res) => {
const { error, value } = userSchema.validate(req.body);
if (error) {
return res.status(400).json({ error: error.details });
}
// Utiliser en toute sécurité les données validées
processUser(value);
});
La validation de schéma garantit que seules les propriétés attendues atteignent votre logique applicative, bloquant ainsi toute tentative de pollution de prototype à l’entrée.
Mettre à jour régulièrement les dépendances
Maintenez vos dépendances à jour, notamment les bibliothèques critiques pour la sécurité. Les versions modernes de bibliothèques populaires comme Lodash ont corrigé les vulnérabilités de pollution de prototype, mais uniquement si vous utilisez des versions récentes. Utilisez des outils comme npm audit ou Snyk pour identifier les dépendances vulnérables :
npm audit
npm audit fix
Activer le mode strict
Le mode strict de JavaScript offre des protections supplémentaires :
'use strict';
// Le mode strict empêche la création accidentelle de variables globales
// et fait en sorte que certaines erreurs silencieuses lèvent des exceptions
function processData(input) {
// Environnement d'exécution plus sûr
}
Tester la pollution de prototype
Incluez des tests de pollution de prototype dans votre suite de tests de sécurité :
describe('Tests de pollution de Prototype', () => {
it('ne doit pas polluer Object.prototype', () => {
const original = Object.prototype.toString;
// Tentative de pollution
const malicious = { "__proto__": { "isAdmin": true } };
merge({}, malicious);
// Vérifier qu'aucune pollution n'a eu lieu
const testObj = {};
expect(testObj.isAdmin).toBeUndefined();
expect(Object.prototype.toString).toBe(original);
});
it('doit rejeter __proto__ dans l’entrée JSON', () => {
const input = '{"__proto__": {"polluted": true}}';
const result = safeJSONParse(input);
expect({}.polluted).toBeUndefined();
});
});
La vision d’ensemble : sécurité par défaut
La pollution de prototype illustre un principe plus large en sécurité applicative : la défense en profondeur. Aucune technique unique ne supprime complètement le risque. Au contraire, il faut empiler plusieurs stratégies de défense :
- Utiliser des objets sans prototype (
Object.create(null)) pour les données utilisateur - Valider et assainir toutes les entrées, notamment les clés d’objet
- Geler les objets et prototypes critiques
- Préférer Map aux objets simples pour les dictionnaires
- Mettre en œuvre une validation stricte du schéma
- Mettre à jour régulièrement les dépendances
- Effectuer des tests de sécurité réguliers
- Surveiller les accès inhabituels aux propriétés en production
Conclusion
La pollution de prototype montre comment une fonctionnalité apparemment anodine du langage peut devenir une vulnérabilité de sécurité grave lorsqu’elle traite des entrées non fiables. En exploitant la chaîne de prototypes de JavaScript, les attaquants peuvent injecter des propriétés qui empoisonnent chaque objet de votre application, conduisant à une escalade de privilèges, une déni de service ou même une exécution de code à distance.
La bonne nouvelle, c’est que la pollution de prototype est évitable grâce à des pratiques de codage disciplinées. En comprenant le vecteur d’attaque, en mettant en œuvre une validation robuste des entrées, en utilisant des patterns de création d’objets plus sûrs et en maintenant vos dépendances à jour, vous pouvez construire des applications JavaScript résistantes à cette vulnérabilité subtile mais dévastatrice.
Souvenez-vous : en JavaScript, votre application n’est aussi sécurisée que sa chaîne de prototypes. Un prototype pollué peut empoisonner toute votre application, mais avec les bonnes stratégies de défense, vous pouvez garantir que vos objets restent purs et que votre application reste sécurisée.
Related InstaTunnel pages
Continue from this article into the most relevant product guides and workflows.
Related Topics
Keep building with InstaTunnel
Read the docs for implementation details or compare plans before you ship.