
TC39 proposal for mitigating prototype pollution
Auteurs : Santiago Díaz (Google)
Champion : Shu-yu Guo (Google)
Stade : 1
freeze, seal et preventExtensions
Cette proposition vise à atténuer une vulnérabilité au niveau du langage connue sous le nom de pollution des prototypes, grâce à un mécanisme qui complète les primitives de gel et à un mécanisme pour rendre la plupart des bases de code compatibles avec elle. Elle décrit une fonctionnalité opt-in qui rend les prototypes disponibles uniquement via les API de réflexion. Ainsi, l'instruction obj[key] ne peut plus accéder aux prototypes. Les bases de code compatibles avec cette fonctionnalité sont plus intentionnelles dans leur façon d'utiliser les prototypes.
Les vulnérabilités PP permettent aux attaquants de manipuler des objets qu'ils ne contrôlent pas ou auxquels ils n'ont pas accès à l'exécution. Cette primitive d'« action fantôme à distance » peut être utilisée pour modifier la forme d'autres objets et remplacer leurs propriétés, contaminant ainsi les objets dans l'environnement d'exécution.
Les objets contaminés invalident les hypothèses sous-jacentes du code qui serait autrement sûr/correct et peuvent conduire à une exécution de code arbitraire et à un large éventail d'autres problèmes de sécurité dans les bases de code JS. Les bugs de pollution des prototypes se manifestent souvent dans les applications web, mais affectent également les environnements d'exécution JS non web.
Les propriétés des objets en JS sont modifiables par tout code qui peut y faire référence. En particulier, si de nombreux objets reposent sur une propriété partagée, n'importe lequel d'entre eux peut entraîner des changements sur tous les autres.
Une propriété particulière de la PP est qu'il s'agit d'une attaque par données uniquement, permettant d'atteindre l'exécution de code purement via les données. Par exemple, voyez le code vulnérable suivant et l'exploit correspondant :
// source is attacker-controlled
function merge(target, source) {
for (let key in source) {
if (typeof source[key] === 'object') {
if(target[key] === undefined) {
target[key] = {};
}
target[key] = merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// User input comes as a string
const userSuppliedObj = JSON.parse('{"__proto__": {"polluted": true}}');
// Trigger prototype pollution
merge({}, userSuppliedObj);
// Create a brand new object
const newObj = {};
// Has polluted property
console.log(newObj.polluted); // true
Notez que l'exploit parvient à contaminer la création de nouveaux objets sans injecter aucun code étranger.
En raison de cette propriété particulière, les atténuations modernes contre les problèmes d'exécution de code — comme la Content Security Policy ou les Trusted Types — ne suffisent pas à protéger contre la PP, car elles se concentrent sur l'application de la provenance du code.
Notez que les attaques par données uniquement sont pertinentes dans les situations où le code exécuté sur la machine virtuelle est fiable et où l'exécution de code arbitraire a un impact sur la sécurité.
freeze, seal et preventExtensionsLes primitives de gel existantes souffrent de problèmes de conception importants qui rendent leur adoption généralisée peu probable. Elles peuvent être utiles aux utilisateurs experts, mais ne conviennent pas à la majorité des développeurs, qui attendent raisonnablement que les prototypes soient mutables :
Les API de gel souffrent de l'erreur d'override et d'autres incohérences qui introduisent des bugs dans les bases de code existantes, les faisant échouer ou, pire, échouer silencieusement en mode non strict. Une enquête précédente sur l'erreur d'override a conclu que l'erreur d'override se déclenche sur ~10 % des bases de code en mode strict et 20 % en mode non strict. L'enquête a été abandonnée peu après.
Les API de gel confient aux développeurs la lourde responsabilité de savoir quels prototypes doivent être gelés pour maintenir une base de code sécurisée, en supposant que les développeurs sont des experts en sécurité. Ces API décrivent le quoi mais pas le comment de la sécurité. Geler Object n'est certainement pas suffisant, car de nombreux exploits abusent d'Array. Qu'en est-il d'Error, Date, Reflect ou Proxy ? Ou des futurs types natifs ? Les API de gel ne fournissent aucune réponse à ces questions.
Les API de gel supposent un point de gel stable : un moment fixe à l'exécution où les prototypes se sont stabilisés et peuvent être gelés. En pratique, ce point est volatile et change au fil du temps dans les bases de code activement développées. Bien que l'on puisse trouver un tel point dans de nombreuses applications aujourd'hui, l'ajout de nouvelles dépendances, de polyfills, de changements de structure de code et de fonctionnalités avancées comme le hotswapping et les outils de développement font des points de gel une cible mouvante.
Les API de gel ne peuvent pas protéger la chaîne de prototypes complète. En JS, des objets peuvent être ajoutés ou retirés de la chaîne de prototypes à tout moment. Pour protéger la chaîne complète, il faut toujours se souvenir de geler les objets ajoutés à la chaîne, un processus sujet aux erreurs. Une fois retirés de la chaîne, ils ne peuvent plus être dégelés.
En résumé : une fonctionnalité qui n'expose les prototypes qu'aux API de réflexion. Si les prototypes n'étaient pas disponibles via des propriétés comme __proto__ ou prototype, ils ne seraient pas exposés aux problèmes liés aux données uniquement.
Ceci est mieux compris à travers un exemple : l'instruction obj[one][two] = value est vulnérable à la PP via obj.__proto__.polluted. Si l'on supprime la propriété Object.prototype.__proto__, la même instruction n'est plus vulnérable car elle ne peut pas utiliser le seul autre moyen d'atteindre les prototypes, à savoir obj.constructor.prototype.polluted. Notez que le prototype ne peut pas être supprimé.
Cette proposition peut être mise en œuvre en fournissant des API de réflexion et en créant une nouvelle fonctionnalité d'encapsulation opt-in qui supprime les propriétés de prototype. Une description de chaque étape suit.
__proto__ est un nom de propriété hérité qui peut être supprimé, mais l'emplacement interne qui le sous-tend peut toujours être lu via Object/Reflect.getPrototypeOf et écrit via Object/Reflect.setPrototypeOf, ce qui continuera simplement à rendre cette propriété accessible au code déjà en cours d'exécution.
Nous proposons la création de nouvelles API pour prototype, par exemple getClassPrototypeOf et setClassPrototypeOf, qui permettraient de supprimer ce nom de propriété sans modifier en quoi que ce soit le fonctionnement de cette propriété spéciale et le support de la VM.
Les API de réflexion peuvent être polyfillées, ce qui permet aux bases de code durcies de fonctionner dans tous les navigateurs, y compris les versions plus anciennes.
Une nouvelle « fonctionnalité d'encapsulation » opt-in où aucun nom de propriété n'est créé pour les fonctions getter et setter des emplacements de prototype, ce qui est désormais possible car les références à ces propriétés peuvent utiliser les API de réflexion à la place.
La fonctionnalité est activée via un drapeau hors bande :
X-Encapsulate-Prototype: true--encapsulate-prototypeLorsque l'encapsulation est désactivée, les prototypes sont disponibles à la fois via les propriétés et les API de réflexion.
Lorsque l'encapsulation est activée, les prototypes ne sont disponibles que via les API de réflexion, après avoir supprimé à la fois __proto__ et prototype.
L'encapsulation inclut également la fonctionnalité de refactorisation automatique suivante :
Lorsque l'encapsulation est activée, les moteurs JS chargeant un nouveau code source activent une étape supplémentaire dans leurs phases d'analyse qui enregistre toute la notation par points vers les propriétés de prototype comme s'il s'agissait d'appels à leurs API de réflexion. Cette étape peut être implémentée efficacement et permet aux bases de code avec des dépendances tierces, transitives ou chargées dynamiquement d'être compatibles avec l'encapsulation.
À l'avenir, ce changement ouvrira la voie au marquage de prototype comme obsolète.
Les propriétés de prototype pourraient simplement être undefined lorsque l'encapsulation est activée, mais elles pourraient lever une erreur en cas de tentative de lecture/écriture. Cela signifierait échouer plus rapidement et de manière bruyante, et permettrait de tester les migrations vers les API de réflexion/encapsulation.
Cela implique de rendre les getters et setters de __proto__ et prototype conditionnels à l'encapsulation, en utilisant un hook hôte, de la même manière que la fonction eval lève une exception sous Content Security Policy.
Le code qui repose sur l'accès par propriété calculée pour référencer les prototypes n'est pas compatible avec l'encapsulation ni avec la refactorisation automatique. Il doit être refactorisé pour faire explicitement référence aux prototypes lorsqu'ils sont utilisés. Cette refactorisation permet en réalité au code d'exprimer son intention, ce qui rend les schémas dangereux visibles pour l'analyse statique. En pratique, les bases de code présentant cette caractéristique sont généralement des frameworks de réflexion, des outils de débogage et d'autres cas d'utilisation fortement basés sur la réflexion, qui sont très probablement conscients de la manière dont ils utilisent les prototypes.
Les bases de code qui utilisent le mot prototype pour définir des propriétés personnalisées ne sont pas compatibles. Ces bases de code peuvent être rendues compatibles avec l'encapsulation si cette propriété est toujours définie/lue via la notation par crochets. Historiquement, et d'après les requêtes HTTP Archive, un faible pourcentage de bases de code sont incompatibles pour cette raison.
Certaines modifications de la propriété constructor peuvent également avoir une action fantôme à distance. Au cours de nos recherches, nous n'avons trouvé aucune vulnérabilité pratique affectée par ce phénomène.
Le seuil est considérablement élevé pour que cette attaque fonctionne : comme dans la PP, il faut trouver une application avec des gadgets permettant à la fois d'écrire et de lire des propriétés arbitraires. Mais dans la pollution du constructeur, le gadget de lecture doit lire depuis constructor.polluted au lieu de polluted. Cela réduit considérablement le nombre de gadgets utiles.
Certains JS minimisés peuvent être incompatibles avec le mode d'encapsulation, car l'accès statique aux propriétés pourrait être minifié en accès calculé. Nous avons interrogé le HTTP Archive pour obtenir une estimation de ce phénomène en pratique. Le tableau suivant montre que les pages présentant ce comportement sont constamment en dessous de 1 % au cours des 12 derniers mois pour toutes les pages explorées avec un navigateur de bureau :
Google a observé une tendance à la hausse des bugs soumis à notre programme de récompenses pour les vulnérabilités : 1 en 2020, 3 en 2021 et 5 jusqu'à présent en 2022. Nous en avons identifié plusieurs autres dans nos recherches internes.
Les exemples de vulnérabilités incluent :
Nous nous attendons à ce que le nombre d'applications vulnérables augmente à mesure que les applications JavaScript sont déployées dans davantage d'environnements (par exemple Electron, Cloudflare Workers, etc.). Par conséquent, une solution au niveau du langage est nécessaire pour atténuer les attaques dans tous les environnements.
| Tableau | Documents accédant dynamiquement à __proto__ ou constructor | Nombre total de documents explorés | Ratio |
|---|
| 2023_03_01_desktop | 5,407,936 | 609,469,458 | 0.89% |
| 2023_02_01_desktop | 4,842,383 | 549,089,708 | 0.88% |
| 2023_01_01_desktop | 5,283,826 | 589,519,160 | 0.90% |
| 2022_12_01_desktop | 5,161,471 | 577,073,883 | 0.89% |
| 2022_11_01_desktop | 5,023,169 | 561,726,239 | 0.89% |
| 2022_10_01_desktop | 4,393,377 | 476,880,624 | 0.92% |
| 2022_09_01_desktop | 4,239,257 | 466,278,762 | 0.91% |
| 2022_08_01_desktop | 4,259,814 | 463,784,047 | 0.92% |
| 2022_07_01_desktop | 3,011,137 | 339,468,615 | 0.89% |
| 2022_06_01_desktop | 2,301,317 | 257,501,222 | 0.89% |
| 2022_04_01_desktop | 2,368,577 | 263,144,657 | 0.90% |
| 2022_03_01_desktop | 2,319,518 | 259,249,013 | 0.89% |