In this article
Comment prévenir les vulnérabilités liées à la pollution des prototypes en JavaScript
La pollution des prototypes est une vulnérabilité JavaScript qui permet aux attaquants d’ajouter des propriétés potentiellement malveillantes aux prototypes d’objets. Lorsque des objets définis par l’utilisateur héritent de ces prototypes, cela peut ouvrir la voie à différentes formes d’attaques.
Dans cet article, vous découvrirez la pollution des prototypes, comment elle est rendue possible, les types d’attaques qu’elle peut faciliter et comment les prévenir. Vous verrez également comment les outils Snyk peuvent vous aider à détecter et à corriger cette vulnérabilité dans votre code et vos dépendances.
Que sont les prototypes en JavaScript ?
JavaScript utilise ce que l’on appelle l’héritage prototypal. Un prototype est un objet qui contient un ensemble de propriétés et de fonctions partagées par toutes les variables du même type. Lorsqu’une variable est déclarée en JavaScript, elle accède automatiquement à un ou plusieurs prototypes.
Par exemple, lorsque vous définissez une variable numérique const one = 1, vous pouvez accéder à la fonction toExponential() du prototype des nombres. Le prototype des nombres possède lui-même un prototype : celui des objets. C’est pourquoi, même lorsque vous travaillez avec un nombre, vous pouvez accéder à hasOwnProperty() : une fonction définie sur le prototype des objets, que le prototype des nombres n’a pas redéfinie.
Lorsqu’une variable numérique possède un prototype qui, à son tour, renvoie à un autre prototype, l’ensemble forme la chaîne de prototypes. Toutes les chaînes de prototypes finissent par remonter jusqu’au prototype des objets.
Dans la plupart des autres langages, les relations d’héritage sont fixées à la compilation. En JavaScript, elles peuvent être modifiées à l’exécution.
Si vous ajoutez une propriété ou une fonction au prototype d’une variable, elle devient alors accessible à toutes les autres variables du même type dans l’application, qu’elles aient été créées avant ou après cette modification.
Si vous ajoutez une propriété ou une fonction au prototype des objets, qui se trouve au sommet de la chaîne d’héritage, elle devient accessible à toutes les autres variables, sauf si celles-ci la redéfinissent.
Voici par exemple un objet JavaScript simple :
const myFirstObject = {};
Pour connaître le prototype de cet objet, vous pouvez utiliser le mot-clé __proto__ :
myFirstObject.__proto__Vous pouvez utiliser le code suivant pour créer un autre objet vide, puis ajouter une propriété à son prototype :
const mySecondObject = {}
mySecondObject.__proto__.customProperty = "this is a property in the prototype"Que se passe-t-il si vous affichez les prototypes de ces deux objets ? Voici le code :
console.log(myFirstObject.__proto__)
console.log(mySecondObject.__proto__)Les deux appels à console.log() affichent le même résultat :
[Object: null prototype] { customProperty: 'this is a property in the prototype' }
[Object: null prototype] { customProperty: 'this is a property in the prototype' }La propriété customProperty que vous avez ajoutée au prototype du deuxième objet apparaît également sur le prototype du premier. Elle est désormais partagée par tous les objets de votre application.
Si vous ajoutez un troisième objet vide comme ceci et souhaitez afficher son prototype, utilisez ce code :
const myThirdObject = {}
console.log(myThirdObject.__proto__)Vous constaterez que customProperty s’y trouve également :
[Object: null prototype] { customProperty: 'this is a property in the prototype' }Si vous définissez une propriété propre à un objet, c’est cette propriété qui sera utilisée :
const nonEmptyObject = {
customProperty: "this is a property defined on the object itself"
}
console.log(nonEmptyObject.customProperty) // "this is a property defined on the object itself"En revanche, si vous utilisez un objet qui ne possède pas la propriété recherchée, l’environnement d’exécution JavaScript la cherchera dans la chaîne de prototypes de l’objet et utilisera la valeur trouvée :
const anotherEmptyObject = {}
console.log(anotherEmptyObject.customProperty) // "this is a property in the prototype"Qu’est-ce que la pollution des prototypes en JavaScript ?
La pollution des prototypes est une vulnérabilité qui permet aux attaquants d’injecter des propriétés et des fonctions malveillantes dans les prototypes des applications JavaScript. Toutefois, la pollution des prototypes n’est pas une attaque en soi : elle facilite différentes attaques. Le prototype des objets est la cible la plus courante, car l’infecter avec du code malveillant permet à l’attaquant d’atteindre un maximum d’éléments.
Le code JavaScript côté serveur comme côté client peut être vulnérable aux attaques par pollution des prototypes. Côté serveur, cette vulnérabilité peut souvent faciliter une élévation de privilèges, un déni de service ou une exécution de code à distance. Côté client, elle peut ouvrir la voie au XSS via le DOM.
Cette vulnérabilité peut affecter les applications écrites en JavaScript ainsi que dans tout langage compilé en JavaScript, notamment TypeScript. La pollution des prototypes peut également se glisser dans votre code et vos dépendances, y compris dans certaines versions de bibliothèques populaires comme Lodash, collection.js et jQuery.
Imaginez qu’un attaquant parvienne à définir la propriété suivante sur le prototype des objets dans votre application JavaScript en cours d’exécution :
isAdmin: true
S’il y parvient, l’attaquant peut examiner le code de votre application à la recherche de contrôles d’accès insuffisamment sécurisés. Si un contrôle s’appuie sur un objet JavaScript représentant l’utilisateur actuel et que cet objet ne possède pas la propriété isAdmin: false, l’environnement d’exécution trouve alors isAdmin: true dans le prototype. L’attaquant peut ainsi facilement élever ses privilèges et accéder à des zones restreintes de votre application.
Mais il ne s’agit pas seulement de définir de nouvelles propriétés sur un prototype. Un attaquant peut également redéfinir des fonctions qui existent déjà sur celui-ci. Par exemple, imaginons qu’un attaquant ait réussi à remplacer la fonction toString() du prototype des objets par un appel récursif :
let newObject = {}
newObject.__proto__.toString = function() {this.toString()}Dès lors, le premier appel à toString() sur un objet, ou même l’interpolation d’un objet dans un littéral de gabarit, fera planter le processus hôte en déclenchant une RangeError: Maximum call stack size exceeded error.
Les attaquants polluent souvent un prototype en exploitant des implémentations non sécurisées de fusion récursive de propriétés dans le code d’applications et de bibliothèques.
La pollution des prototypes en pratique
Testons une application exemple pour vous aider à comprendre le fonctionnement de la pollution des prototypes dans Node.js. Vous allez utiliser une application Express, une base de données MongoDB en mémoire et quelques points de terminaison.
Clonez ce dépôt pour suivre les étapes. Une fois le dépôt cloné, restaurez les dépendances et lancez l’application :
npm install
npm startOuvrez http://localhost:8080/todos/ dans votre navigateur ou envoyez une requête GET à http://localhost:8080/todos/ depuis le client HTTP de votre choix. Vous obtiendrez un tableau contenant trois tâches :
[
{
"_id": "654c062468efa31d8c055c13",
"text": "Jason's first todo item",
"open": true,
"visible": true,
"owner": "654c062468efa31d8c055c0e",
"__v": 0
},
{
"_id": "654c062468efa31d8c055c14",
"text": "First todo for Kelly",
"open": true,
"visible": true,
"owner": "654c062468efa31d8c055c0f",
"__v": 0
},
{
"_id": "654c062468efa31d8c055c15",
"text": "Paul's thing to be done",
"open": true,
"visible": true,
"owner": "654c062468efa31d8c055c10",
"__v": 0
}
]
Ouvrez maintenant database/seed.js dans le dépôt cloné. Vous verrez que la base de données de l’application contient non pas trois, mais quatre tâches :
const seedTodoItems = [
{
text: "Jason's first todo item",
open: true,
visible: true,
owner: "Jason"
},
{
text: "First todo for Kelly",
open: true,
visible: true,
owner: "Kelly"
},
{
text: "Paul's thing to be done",
open: true,
visible: true,
owner: "Paul"
},
{
text: "A HIDDEN TODO",
open: true,
owner: "Alexandra"
}
]
Trois tâches initialisées sont explicitement définies comme visibles, mais pas la quatrième, qui est donc masquée par défaut. Si vous ouvrez server.js et examinez le point de terminaison qui traite la requête GET, vous verrez qu’il ne renvoie que les tâches visibles :
app.get('/todos/', (req, res) => {
TodoItem.find({visible: true})
.then(data => res.json(data))
.catch(error => res.json({error}))
})Utilisez un autre point de terminaison pour ajouter une tâche. Envoyez la requête POST suivante à http://localhost:8080/todos/add:
POST http://localhost:8080/todos/add
Content-Type: application/json
{
"__proto__": {
"visible": true
},
"text": "😈 new todo item that tries to mess with the prototype",
"owner": "654c0cc960d82b5802a0b30d"
}Vous pouvez utiliser la commande curl suivante dans votre terminal pour créer une tâche :
curl -X POST -H 'content-type: application/json' http://localhost:8080/todos/ad
d --data '{ "__proto__": {
"visible": true
},
"text": "😈 new todo item that tries to mess with the prototype",
"owner": "654c0cc960d82b5802a0b30d"
}'Envoyez à nouveau une requête GET à http://localhost:8080/todos/. Voici le résultat :
[
{
"_id": "654c0d59654aeeb63dfd5ae8",
"text": "Jason's first todo item",
"open": true,
"visible": true,
"owner": "654c0d59654aeeb63dfd5ae3",
"__v": 0
},
{
"_id": "654c0d59654aeeb63dfd5ae9",
"text": "First todo for Kelly",
"open": true,
"visible": true,
"owner": "654c0d59654aeeb63dfd5ae4",
"__v": 0
},
{
"_id": "654c0d59654aeeb63dfd5aea",
"text": "Paul's thing to be done",
"open": true,
"visible": true,
"owner": "654c0d59654aeeb63dfd5ae5",
"__v": 0
},
{
"_id": "654c0d59654aeeb63dfd5aeb",
"text": "A HIDDEN TODO",
"open": true,
"owner": "654c0d59654aeeb63dfd5ae6",
"__v": 0
},
{
"_id": "654c0e6f654aeeb63dfd5aef",
"text": "😈 new todo item that tries to mess with the prototype",
"open": true,
"owner": "654c0cc960d82b5802a0b30d",
"__v": 0
}
]
Au départ, vous aviez trois tâches visibles. Vous en avez ajouté une autre. Pourtant, il y en a maintenant cinq au lieu de quatre. La tâche qui devait rester masquée est désormais visible !
Examinons ce qui se passe dans le point de terminaison qui ajoute une tâche :
app.post('/todos/add', (req, res) => {
const defaults = {
open: true,
}
const todoToAdd = lodash.merge(defaults, req.body)
const todoItem = new TodoItem(todoToAdd);
todoItem.save()
.then(() => res.json({msg: `Successfully added a todo item`}))
.catch(error => res.json({error: error.message}))
})
L’application analyse le corps de la requête POST (un objet JSON) et le fusionne avec l’objet defaults qui sert de base à chaque nouvelle tâche. Pour effectuer cette fusion, elle utilise la méthode merge() d’une version vulnérable de Lodash. La partie malveillante de la requête, qui modifie le prototype des objets, est donc fusionnée sans difficulté dans l’objet obtenu et définit au passage Object.__proto__.visible à true.
Par conséquent, tous les objets existants et futurs qui ne déclarent pas explicitement de propriété visible commencent à l’hériter de leur prototype, qui la possède. Lorsque l’application interroge la base de données en traitant une requête GET, elle utilise un filtre censé ne récupérer que les tâches visibles :
app.get('/todos/', (req, res) => {
TodoItem.find({visible: true})
.then(data => res.json(data))
.catch(error => res.json({error}))
})
Lors de l’évaluation du filtre, il s’avère que la tâche masquée ne possède pas sa propre propriété visible: false :
{
text: "A HIDDEN TODO",
open: true,
owner: "Alexandra"
}Or, le prototype possède désormais la valeur visible: true. Cette valeur est alors utilisée, ce qui entraîne la divulgation involontaire d’une tâche préexistante qui devait rester masquée. De plus, la tâche ajoutée dans le cadre de la requête malveillante est révélée, même si elle ne possède pas la propriété visible: true.
Prévenir les vulnérabilités liées à la pollution des prototypes
Dans l’application exemple précédente, il suffit de mettre à niveau Lodash vers une version plus récente pour corriger la vulnérabilité. Il existe toutefois d’autres moyens de prévenir la pollution des prototypes, qu’il est important de connaître, notamment lorsque vous examinez votre propre code plutôt que celui d’une bibliothèque.
Assainir les clés de propriété
La solution la plus évidente consiste à vérifier qu’une clé peut être fusionnée en toute sécurité avant de procéder à la fusion :
if (key == '__proto__') {
return;
}Cependant, il n’est pas idéal de maintenir une liste de refus, car il existe des moyens connus de contourner cette protection.
Il est préférable de maintenir une liste d’autorisation des clés de propriété permises :
if (allowedKeys.includes(key)) {
// Proceed with merge
}Utiliser Map plutôt qu’un objet
JavaScript propose une autre structure pour stocker des paires clé-valeur : Map. Bien qu’elle ressemble à un objet à bien des égards, elle présente une différence majeure : lorsque vous utilisez la fonction get() d’une map, vous ne pouvez récupérer que les éléments que vous y avez explicitement ajoutés. Les éléments malveillants qui auraient pu être ajoutés au prototype des objets ne seront pas accessibles :
const defaultsMap = new Map()
defaultsMap.set("open", true);
defaultsMap.set("hidden", false)
// vs
const defaultsObject = {
open: true,
hidden: false
}Utiliser Object.freeze() ou Object.seal() pour empêcher les modifications
JavaScript fournit deux fonctions intégrées qui limitent les modifications pouvant être apportées aux objets : Object.freeze() et Object.seal(). Le prototype des objets étant lui-même un objet, vous pouvez lui appliquer ces fonctions :
Object.freeze(Object.prototype)Pour prévenir la pollution des prototypes, il est préférable d’utiliser Object.freeze(), car cette fonction empêche à la fois l’ajout de nouvelles propriétés et la modification des propriétés existantes d’un objet.
Object.seal() empêche uniquement l’ajout de nouvelles propriétés, mais autorise la modification des propriétés existantes. Si vous utilisez Object.seal(), la pollution des prototypes reste donc possible, par exemple en redéfinissant des fonctions du prototype des objets comme toString().
Utiliser le package nopp de Snyk
Le package nopp de Snyk prolonge efficacement le principe du gel des objets. Il applique Object.freeze() à certains objets intégrés de JavaScript. Utilisé vers la fin de l’initialisation de votre application, il autorise les modifications légitimes des prototypes, tout en bloquant les tentatives malveillantes de pollution lorsque l’application est en cours d’exécution.
Dans l’application Express exemple utilisée précédemment, il suffit d’installer nopp pour l’ajouter :
npm install noppPuis ajoutez-le en dernier dans les instructions d’importation de server.js :
import express from 'express';
import connect from "./database/connection.js";
import seedDatabase from "./database/seed.js";
import TodoItem from "./model/todoitem.model.js";
import lodash from "lodash";
import cors from "cors";
import "nopp";Dès que nopp est ajouté, le scénario de pollution des prototypes menant à une élévation de privilèges est atténué, même si l’application exemple continue d’utiliser une version vulnérable de Lodash.
Détecter les vulnérabilités liées à la pollution des prototypes avec l’extension IDE de Snyk
N’oubliez pas qu’il est plus facile de corriger les vulnérabilités, notamment celles liées à la pollution des prototypes, lorsqu’elles sont faciles à détecter. Un outil qui signale les problèmes de sécurité potentiels dans votre éditeur de code peut grandement vous aider à livrer du code JavaScript sécurisé.
Si un tel outil vous intéresse, pensez à installer l’extension Snyk Security pour Visual Studio Code. (Si vous utilisez un IDE JetBrains comme WebStorm, une extension est également disponible.)
L’extension Snyk repère et signale les risques de pollution des prototypes et d’autres types de vulnérabilités dans votre code JavaScript et vos bibliothèques. Pour chaque vulnérabilité détectée, elle montre comment différents projets open source ont corrigé des problèmes similaires.
Voici une capture d’écran de l’extension IDE Snyk pour VS Code en action. Elle détecte plusieurs vulnérabilités dans le package npm lodash, notamment la pollution des prototypes, le déni de service par expression régulière et l’injection de commandes :

Pour installer l’extension Snyk, recherchez « snyk » dans le panneau Extensions de Visual Studio Code :

Installez l’extension « Snyk Security - Code, Open Source Dependencies, IaC Configurations » :

L’extension Snyk est installée avec le Snyk CLI, dont vous aurez besoin pour lancer les analyses Snyk par la suite.
Cliquez maintenant sur l’icône Snyk dans la barre de menu à gauche de Visual Studio Code, puis, dans le panneau Snyk, cliquez sur « Trust workspace and connect » :

Snyk ouvre une fenêtre de navigateur dans laquelle vous devez vous connecter à votre compte Snyk ou en créer un :

Une fois connecté, Snyk doit authentifier votre machine pour associer votre Snyk CLI local à votre compte Snyk :

Après avoir cliqué sur « Authenticate », l’application web Snyk vérifie que votre authentification a bien abouti :

Vous pouvez maintenant fermer la fenêtre du navigateur et revenir à Visual Studio Code. Dès que vous ouvrez un espace de travail ou un dossier, Snyk lance l’analyse des vulnérabilités.
Voici à quoi peut ressembler Visual Studio Code après l’analyse d’un projet par l’extension Snyk :

Dans le panneau « SNYK » à gauche, vous verrez la liste des problèmes de sécurité et de qualité du code détectés, notamment la pollution de prototype, le déni de service par expression régulière (ReDoS), la falsification de requête intersite (CSRF) et l’exposition d’informations.
Dans l’onglet « Editor », les lignes de code concernées sont soulignées. Appuyez sur Ctrl +. (ou Cmd + . sur Mac) pour afficher un menu d’actions rapides associées. Dans un panneau distinct, l’extension Snyk résume la vulnérabilité détectée et présente des exemples de résolution de vulnérabilités similaires dans divers projets open source.
La flexibilité de JavaScript a un coût, et la pollution de prototype illustre parfaitement à quel point JavaScript peut être détourné si les développeurs ne prennent pas les mesures nécessaires pour sécuriser leur code.
La meilleure façon d’apprendre la sécurité des applications est de pratiquer en codant. Découvrez la leçon Snyk Learn sur la pollution de prototype, qui propose des exemples de code interactifs !

Les extensions IDE Snyk Security vous aident à détecter la pollution de prototype et d’autres vulnérabilités dès les premières étapes, sans quitter votre éditeur de code préféré.
En tirant parti des aspects les plus sûrs de JavaScript et d’outils de développement performants, vous pouvez livrer du code JavaScript sécurisé et éviter des incidents de sécurité coûteux.
Sécurisez vos applications JavaScript dès maintenant
Détectez et corrigez gratuitement les vulnérabilités JavaScript avec Snyk.
Aucune carte de crédit requise.
Ou inscrivez-vous avec Azure AD Docker ID Bitbucket
En utilisant Snyk, vous acceptez de respecter nos politiques, notamment nos Conditions d’utilisation et notre Politique de confidentialité.