Confusion de types en JavaScript : contourner la validation des entrées (et corriger le problème)
Alessio Della Libera
3 novembre 2021
0 minutes de lectureDans un précédent article de blog, nous avons montré comment la manipulation des types (ou confusion de types) peut permettre de sortir des sandbox de templates, entraînant des vulnérabilités de cross-site scripting (XSS) ou d’injection de code.
L’un des principaux objectifs de cette recherche était d’examiner, dans l’écosystème JavaScript, s’il est possible de contourner certains correctifs de sécurité ou certaines validations d’entrée au moyen d’une attaque par confusion de types (c.-à-d. en fournissant un type d’entrée inattendu).
Nous avons commencé nos recherches en examinant comment sont corrigées des vulnérabilités courantes, comme la pollution de prototype et les attaques XSS. Pour cela, nous avons exploité les données de notre base de données des vulnérabilités Snyk Intel, la plus grande base de données de vulnérabilités open source du secteur. Ces informations nous ont permis de repérer des approches récurrentes utilisées par différents responsables de projets pour prévenir certaines catégories de vulnérabilités.
Il est maintenant temps d’examiner les vulnérabilités liées à la confusion de types. Dans cet article de blog, nous présentons des scénarios courants où la désinfection et la validation des entrées peuvent être contournées en fournissant un type inattendu. Nous proposons également des exemples de mesures correctives et des suggestions à l’intention des responsables de projets open source qui souhaitent corriger ces contournements de la validation des entrées.
Notions de base sur JavaScript
Avant d’expliquer comment une valeur de type tableau peut servir à contourner certaines validations d’entrée et entraîner une éventuelle vulnérabilité de sécurité, commençons par revoir quelques notions fondamentales du fonctionnement de JavaScript. Elles nous seront très utiles par la suite, notamment lorsque nous étudierons le cas de la pollution de prototype.
Comparaison de valeurs : === et ==
En JavaScript, deux opérateurs permettent de comparer des valeurs :
Égalité stricte
===Égalité souple
==
L’une des principales différences entre ces opérateurs est que === renvoie toujours false si les deux opérandes sont de types différents (il n’effectue aucune coercition). Avec l’opérateur ==, si les opérandes sont de types différents, ils sont d’abord convertis vers un type commun, puis comparés. Pour en savoir plus sur le fonctionnement de ces opérateurs, consultez la documentation officielle sur l’opérateur d’égalité stricte et l’opérateur d’égalité souple.
L’exemple suivant illustre ce comportement :
Lorsque les opérandes comparés ont la même valeur et le même type, les opérateurs == et === renvoient tous deux true (lignes 2 et 3). En revanche, si nous comparons un tableau dont la représentation sous forme de chaîne est identique à celle de l’autre opérande, le résultat ne sera true qu’avec l’opérateur ==. À la ligne 6, la valeur ["test"] est d’abord convertie en chaîne, puis comparée à l’autre opérande, "test". Lorsque les opérandes sont de types différents, l’opérateur === renvoie toujours false (ligne 7).
Remarque : Les considérations ci-dessus s’appliquent également aux opérateurs d’inégalité !== et !=.
Voici quelques façons d’obtenir la représentation sous forme de chaîne d’une valeur :
appeler la méthode
toString()(ligne 2)convertir en chaîne à l’aide de la fonction
String(ligne 2)concaténer la valeur avec une chaîne vide à l’aide de l’opérateur
+(ligne 3)
Pour un tableau, sa représentation sous forme de chaîne correspond à la concaténation de ses éléments, séparés par des virgules.
Accesseur de propriété
Autre fonctionnalité importante de JavaScript : avec la notation entre crochets, il est possible d’utiliser des valeurs de différents types (pas seulement des chaînes) pour accéder aux propriétés d’un objet. Si la valeur n’est pas une chaîne (ou un Symbol), elle est d’abord convertie en chaîne, puis utilisée pour accéder à la propriété de l’objet.
Prenons l’exemple suivant :
Comme nous le voyons, la clé utilisée pour accéder à une propriété peut être de différents types. À la ligne 4, la propriété ["test"] est d’abord convertie en chaîne (c’est-à-dire en valeur "test"), puis le résultat de cette opération est utilisé comme clé pour accéder à la propriété de l’objet. En effet, si nous utilisons la chaîne "test" à la ligne 6, nous pouvons obtenir la valeur définie à la ligne 4.
De la même manière, si nous utilisons un objet vide {} (ligne 9), dont la représentation sous forme de chaîne est [object Object], pour écrire une valeur d’objet, nous pouvons ensuite accéder à cette même valeur directement avec la chaîne [object Object] (ligne 11).
Méthodes String et Array
Certaines méthodes intégrées sont définies à la fois sur String et Array et portent le même nom. Citons par exemple includes, indexOf, lastIndexOf, etc.
Pourquoi est-ce important dans le cadre de cet article ? Le comportement de ces méthodes varie selon le type de l’entrée sur laquelle elles sont appelées. Si l’une d’elles sert à valider une entrée utilisateur, cette validation peut être contournée en fournissant une entrée d’un autre type (par exemple, un tableau) sans effectuer de vérifications.
L’exemple suivant montre que les méthodes intégrées définies à la fois sur String et Array ont des comportements différents :
Remarque : Toutes les valeurs de l’exemple ("<script>", ["<script>"] et [["<script>"]]) ont la même représentation sous forme de chaîne.
Par exemple, à la ligne 3, la méthode appelée est String.prototype.indexOf(). Elle vérifie si le caractère < se trouve dans la chaîne, puis renvoie l’indice de sa première occurrence. Sinon, elle renvoie -1.
En revanche, si l’entrée est un tableau, la méthode appelée est Array.prototype.indexOf() ; elle vérifie si l’élément < se trouve dans le tableau. Comme celui-ci ne contient qu’un seul élément (la chaîne "<script>"), la fonction renvoie -1, empêchant ainsi la désinfection de ce caractère.
Résumé des notions fondamentales de JavaScript
Récapitulons ce que nous avons vu :
Si les opérandes sont de types différents, l’opérateur
===renvoie toujoursfalseIl est possible d’utiliser des clés de différents types pour accéder aux propriétés d’un objet (avec la notation entre crochets)
Certaines méthodes intégrées définies à la fois sur les types
StringetArray(commeindexOfouincludes) peuvent avoir un comportement différent selon le type de l’entrée
Avant de poursuivre, examinons les scénarios suivants où certaines des méthodes ci-dessus sont utilisées pour valider une entrée utilisateur.
La vérification suivante peut ne pas suffire à empêcher la définition de la clé isAdmin sur l’objet obj (nous supposons que la variable prop est contrôlée par l’utilisateur) :
Si la valeur de prop est remplacée par "isAdmin", l’exemple ci-dessus déclenche une exception.
Voici un autre exemple de vérification « faible » destinée à prévenir une éventuelle XSS en détectant la présence de certains caractères dangereux dans l’entrée :
Il est important de noter que ces contournements ne sont possibles que sous certaines conditions. Par exemple, l’entrée doit provenir de sources spécifiques (voir la section suivante) et aucune méthode intégrée définie uniquement sur String ne doit être appelée sur l’entrée ; sinon, l’application renverra une erreur ou déclenchera une exception (car ces méthodes ne sont pas définies pour les autres types).
Dans l’exemple suivant, la méthode toLowerCase() est appelée sur l’entrée. Nous avons vu qu’il est possible de contourner la comparaison === avec une valeur de type tableau. Toutefois, si un tableau est fourni, l’application déclenche une exception, car la méthode toLowerCase() n’est définie que pour les chaînes.
Comment obtenir des valeurs de type tableau
À ce stade, vous vous demandez peut-être comment obtenir des valeurs de type tableau à partir de données distantes.
Il existe plusieurs façons d’obtenir des valeurs de type tableau :
Côté serveur : avec le framework populaire Express, les valeurs issues de
req.queryou dereq.body(si le middlewareexpress.json()est utilisé) peuvent également être analysées comme des tableaux ou des objets (et pas seulement comme des chaînes).Côté client : données provenant de l’API
postMessage.
Côté serveur
Pour les requêtes GET, avec le framework populaire Express et si la valeur provient de req.query, la notation parameter[]=value permet d’obtenir des valeurs de type tableau. Cela peut généralement s’appliquer à toute application qui utilise la bibliothèque populaire qs pour analyser la querystring.
Pour les requêtes POST, si le middleware express.json() est utilisé, le corps de la requête est analysé en tant qu’objet JSON.
L’application Express suivante montre comment obtenir des valeurs de type tableau :
Côté client
Les données provenant de l’API postMessage peuvent également être de différents types (et pas seulement des chaînes) :
Pour tester le code ci-dessus, ouvrez la console développeur du navigateur et exécutez : window.postMessage({name: ["array"]}, "*"). Le résultat suivant devrait s’afficher :

Résultats
Dans le cadre de cette recherche, nous avons découvert et divulgué plusieurs problèmes :
Module | Vulnérabilité | Avis Snyk | CVE |
|---|---|---|---|
object-path | Pollution de prototype | CVE-2021-23434 | |
immer | Pollution de prototype | CVE-2021-23436 | |
mpath | Pollution de prototype | CVE-2021-23438 | |
set-value | Pollution de prototype | CVE-2021-23440 | |
edge.js | Cross-site scripting (XSS) | CVE-2021-23443 | |
jointjs | Pollution de prototype | CVE-2021-23444 | |
datatables.net | Cross-site scripting (XSS) | CVE-2021-23445 | |
teddy | Cross-site scripting (XSS) | CVE-2021-23447 |
Nous avons contacté plusieurs responsables de projets de manière confidentielle et responsable, conformément à notre processus de divulgation des vulnérabilités (certains problèmes sont toujours en cours de divulgation).
Nous tenons surtout à remercier tous les responsables de projets que nous avons contactés pour le temps qu’ils nous ont consacré.
Dans les études de cas suivantes, nous expliquerons comment des valeurs de type tableau ont permis de contourner un correctif contre la pollution de prototype et une validation des entrées visant à prévenir les attaques XSS.
Étude de cas : pollution de prototype dans object-path
object-path est une bibliothèque qui permet d’accéder à des propriétés imbriquées à l’aide d’un chemin. Elle accepte également un chemin sous forme de tableau, ce qui permet de fournir des chemins sous la forme ['part1', 'part2', etc.]. Cette bibliothèque était déjà vulnérable à une pollution de prototype (CVE-2020-15256). Le correctif introduit est le suivant :
Ce correctif déclenche correctement une exception lorsque le chemin contient l’un de ces composants dangereux (sous forme de chaîne). En effet, avec la charge utile suivante, la fonction renvoie une erreur :
Cependant, comme nous l’avons vu précédemment :
Avec la notation entre crochets, une clé de n’importe quel type peut être utilisée pour accéder aux propriétés d’un objet
L’opérateur
===renvoiefalsesi les opérandes sont de types différents. La conditioncurrentPath === '__proto__'renverrafalsesicurrentPathvaut['__proto__'](la même chose s’applique àconstructor).
Cela signifie qu’en plaçant la clé dangereuse dans un tableau à un seul élément (la clé elle-même), on peut contourner le correctif et provoquer malgré tout une pollution de prototype :
Mesure corrective
Snyk a contacté le responsable du projet en privé le 25 août 2021. Le problème a été rapidement corrigé le 27 août 2021 dans la version v0.11.6. Le correctif introduit empêche ce scénario en convertissant les composants du chemin en chaînes (s’ils ne sont ni de type nombre ni de type chaîne) avant de les vérifier :
La charge utile précédente ne fonctionnera plus :
Étude de cas : faille XSS (Cross-Site Scripting) dans edge.js
edge.js est un moteur de templating Node.js. Cette bibliothèque intègre des fonctionnalités qui peuvent être activées pour prévenir les failles de sécurité, comme les attaques XSS. En particulier, selon la documentation, « la sortie de l’interpolation (le code entre accolades) est échappée au format HTML afin d’éviter les attaques XSS ». La fonction d’origine chargée d’échapper les caractères HTML dangereux pour prévenir les attaques XSS est la suivante :
Comme nous pouvons le constater, input n’est échappé au format HTML que s’il est de type string. Cela signifie que si la donnée contrôlée par l’utilisateur est de type objet (c’est-à-dire un tableau) et n’est pas une SafeValue, elle est renvoyée sans être échappée, même si {{ }} est utilisé dans le modèle (sans erreur ni exception), ce qui peut entraîner une faille XSS.
Pour montrer comment exploiter cette faille, prenons l’exemple de l’application web suivante, qui utilise cette bibliothèque comme moteur de modèles pour afficher une valeur contrôlée par l’utilisateur dans un modèle :
Le contenu du fichier views/welcome.edge est le suivant :
Comme nous l’avons vu, les données provenant de certaines sources peuvent également être de différents types. En accédant à l’URL http://localhost:3000/test?name=%3Cimg%20src=x%20onerror=%27alert(1)%27%20/%3E, la fonction échappe la donnée saisie. Cependant, en accédant à cette autre URL (notez les crochets []) http://localhost:3000/test?name[]=%3Cimg%20src=x%20onerror=%27alert(1)%27%20/%3E, il est possible de déclencher une attaque XSS, car le paramètre req.query.name sera analysé comme un tableau et ne sera donc pas échappé.
Correctif
Snyk a contacté le responsable de la maintenance en privé le 1er septembre 2021, et le problème a été rapidement corrigé dans la version v5.3.2. Le correctif introduit échappe toutes les données qui ne sont pas de type SafeValue pour remédier à ce scénario :
Pour prévenir ce type de scénario, on peut aussi convertir la donnée en chaîne avant de l’échapper (et ainsi éviter de vérifier son type comme dans l’exemple ci-dessus) ou renvoyer une chaîne vide (ou un message d’erreur) si elle n’est pas de type chaîne.
À retenir pour…
Les développeurs
Lorsque vous utilisez l’opérateur === pour effectuer une désinfection, il est important de vérifier que les deux opérandes sont du même type. Lors de l’échappement d’une entrée, il est également important de traiter le cas où elle n’est pas de type chaîne (en particulier si le comportement de la fonction avec des valeurs non textuelles n’est pas documenté).
Les responsables de la maintenance
Si une fonction ou une API est chargée de désinfecter les entrées et que leur type n’est pas pris en charge, il est utile de documenter ce comportement afin d’éviter toute confusion. Le responsable de la maintenance peut traiter certains problèmes de désinfection (par exemple, échapper l’entrée si elle est une chaîne), mais pas d’autres (comme le cas où elle n’est pas une chaîne).
Les chercheurs en sécurité
Nous nous sommes concentrés sur les vulnérabilités de pollution de prototypes et XSS, mais nous pensons que d’autres cas ou scénarios pourraient permettre d’utiliser ce vecteur d’attaque pour contourner des mécanismes de désinfection existants et entraîner des problèmes de sécurité. Si vous découvrez des problèmes similaires (ou toute autre vulnérabilité) dans un projet open source pris en charge par notre programme, n’hésitez pas à nous les signaler à l’aide du formulaire de divulgation des vulnérabilités de Snyk.
Références
Pollution de prototypes : Arteau, Oliver. « JavaScript prototype pollution attack in NodeJS application ». GitHub, 26 mai 2018
Lancez-vous dans les compétitions Capture The Flag
Apprenez à résoudre des défis de capture du drapeau en regardant à la demande notre atelier virtuel d’initiation.



