
Proposta TC39 per mitigare l'inquinamento del prototipo
Autori: Santiago Díaz (Google)
Champion: Shu-yu Guo (Google)
Stage: 1
freeze, seal e preventExtensions
Questa proposta mira a mitigare una vulnerabilità a livello di linguaggio nota come prototype pollution con un meccanismo che completa le primitive di freeze e un meccanismo per rendere la maggior parte delle codebase compatibili con essa. Descrive una funzionalità opt-in che rende i prototipi disponibili solo tramite API di reflection. In questo modo, l'affermazione obj[key] non può più accedere ai prototipi. Le codebase compatibili con questa funzionalità sono più intenzionali nel modo in cui usano i prototipi.
Le vulnerabilità PP consentono agli attaccanti di manipolare oggetti che non controllano o a cui non hanno accesso in fase di esecuzione. Questa primitiva di 'azione spettrale a distanza' può essere usata per modificare la forma di altri oggetti e sovrascrivere le loro proprietà, contaminando così gli oggetti nel runtime.
Gli oggetti contaminati invalidano le assunzioni di base del codice che altrimenti sarebbe sicuro/corretto e possono portare all'esecuzione di codice arbitrario e a un'ampia gamma di altri problemi di sicurezza nelle codebase JS. I bug di prototype pollution si manifestano spesso nelle applicazioni web, ma colpiscono anche i runtime JS non web.
Le proprietà degli oggetti in JS sono scrivibili da qualsiasi codice possa referenziarle. In particolare, se molti oggetti dipendono da una proprietà condivisa, uno qualsiasi di essi può apportare modifiche a tutti gli altri.
Una proprietà speciale della PP è che si tratta di un attacco basato solo sui dati, che consente di ottenere l'esecuzione di codice puramente tramite dati. Ad esempio, si veda il seguente codice vulnerabile e il corrispondente exploit:
// 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
Notare che l'exploit è in grado di contaminare la creazione di nuovi oggetti senza iniettare alcun codice esterno.
A causa di questa proprietà speciale, le mitigazioni moderne contro i problemi di esecuzione del codice -come la Content Security Policy o i Trusted Types- non sono sufficienti a proteggere dalla PP, poiché si concentrano sull'applicazione della provenienza del codice.
Nota che gli attacchi basati solo sui dati sono rilevanti in situazioni in cui il codice in esecuzione sulla VM è considerato fidato e l'esecuzione di codice arbitrario ha un impatto sulla sicurezza.
freeze, seal e preventExtensionsLe primitive di freeze esistenti soffrono di significativi problemi di progettazione che rendono improbabile la loro adozione su larga scala. Possono essere utili agli utenti esperti, ma non sono adatte a essere impiegate dalla maggior parte degli sviluppatori, che ragionevolmente si aspettano che i prototipi siano mutabili:
Le API di freeze soffrono dell'errore di override e di altre incongruenze che introducono bug nelle codebase esistenti, facendole generare eccezioni o, peggio, fallire silenziosamente in modalità sloppy. Una precedente indagine sull'errore di override ha concluso che l'errore di override si verifica in ~10% delle codebase in modalità strict e nel 20% in modalità sloppy. L'indagine è stata abbandonata poco dopo.
Le API di freeze danno agli sviluppatori la pesante responsabilità di sapere quali prototipi dovrebbero essere congelati per mantenere una codebase sicura, presupponendo che gli sviluppatori siano esperti di sicurezza. Queste API descrivono il cosa ma non il come della sicurezza. Congelare Object non è certamente sufficiente, poiché molti exploit abusano di Array. E che dire di Error, Date, Reflect o Proxy? O dei futuri tipi built-in? Le API di freeze non forniscono risposte a queste domande.
Le API di freeze presuppongono un punto di congelamento stabile: un momento fisso nel runtime in cui i prototipi si sono assestati e possono essere congelati. In pratica, questo punto è volatile e cambia nel tempo nelle codebase sviluppate attivamente. Sebbene oggi sia possibile trovare un punto del genere in molte applicazioni, l'aggiunta di nuove dipendenze, polyfill, modifiche alla struttura del codice e funzionalità avanzate come l'hot swapping e gli strumenti per sviluppatori rendono i punti di congelamento un bersaglio mobile.
Le API di freeze non possono proteggere l'intera catena dei prototipi. In JS, gli oggetti possono essere aggiunti o rimossi dalla catena dei prototipi in qualsiasi momento. Per proteggere l'intera catena, bisognerebbe ricordarsi sempre di congelare gli oggetti aggiunti alla catena, un processo soggetto a errori. Quando vengono rimossi dalla catena, non possono più essere "scongelati".
In poche parole: una funzionalità che espone i prototipi solo alle API di reflection. Se i prototipi non fossero resi disponibili tramite proprietà come __proto__ o prototype, non sarebbero esposti a problemi basati solo sui dati.
Questo si capisce meglio con un esempio: l'affermazione obj[one][two] = value è vulnerabile alla PP tramite obj.__proto__.polluted. Se si elimina la proprietà Object.prototype.__proto__, la stessa affermazione non è più vulnerabile perché non può sfruttare l'unico altro modo per raggiungere i prototipi, cioè obj.constructor.prototype.polluted. Notare che prototype non può essere eliminato.
Questa proposta può essere implementata fornendo API di reflection e creando una nuova funzionalità di incapsulamento opt-in che elimina le proprietà dei prototipi. Di seguito una descrizione di ciascun passaggio.