JSON Toolkit

Che cos'è l'iniezione di __proto__

In sintesi: JSON.parse tratta __proto__ come una chiave ordinaria; il danno avviene in un merge ricorsivo che scrive su Object.prototype, quindi ogni oggetto eredita le proprietà dell'attaccante.

Avanzato~5 min di letturaPubblicato il 2026-10-30Aggiornato il 2026-10-30
Come funziona l'inquinamento del prototipo e come bloccarlo

In sintesi: blocca __proto__, constructor e prototype prima di qualsiasi merge, unisci input non fidati in Object.create(null) e aggiungi un'asserzione sul prototipo in CI; aggiornare le librerie da solo non basta.

In una frase: una chiave non fidatata chiamata __proto__ incontra un merge ricorsivo, e il merge scrive sul prototipo.

Una riproduzione

function merge(target, source) {
  for (const key in source) {
    if (typeof source[key] === 'object' && source[key] !== null) {
      target[key] = target[key] || {};
      merge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

merge({}, JSON.parse('{"__proto__": {"isAdmin": true}}'));
console.log({}.isAdmin);   // true

Nessun oggetto di business è stato toccato. È stato toccato Object.prototype, e ogni oggetto ora porta isAdmin.

Perché JSON.parse non ha colpe

const data = JSON.parse('{"__proto__": {"a": 1}}');
Object.getOwnPropertyNames(data);                  // [ '__proto__' ]
Object.getPrototypeOf(data) === Object.prototype;  // true, untouched

__proto__ arriva come una normale proprietà propria. Il pericolo è interamente nel codice che in seguito fa merge, deep copy o set per percorso — _.merge, alcuni pattern di Object.assign e innumerevoli helper set(obj, path, value) scritti a mano.

Impatto

  • Bypass dell'autorizzazione: if (user.isAdmin) diventa incondizionatamente vero
  • Corruzione dei dati: campi inattesi vengono serializzati nei record memorizzati
  • Denial of service: grandi oggetti attaccati al prototipo fanno crescere permanentemente la memoria
  • Escalation: combinato con motori di template o runner di comandi, ha portato all'esecuzione di codice in remoto

Quattro difese

Filtra le chiavi.

const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);

Bloccare solo __proto__ è insufficiente: constructor.prototype raggiunge lo stesso punto.

Unisci in un oggetto senza prototipo.

const safeTarget = Object.create(null);

Senza catena di prototipi, __proto__ è solo una chiave stringa.

Usa Object.hasOwn invece di in. in consulta la catena di prototipi.

Mantieni aggiornate le dipendenze. lodash, deepmerge e json-patch hanno tutti pubblicato correzioni per questa classe di bug. Tieni d'occhio npm audit.

Verificare che resti corretto

Passa un payload che contiene __proto__ e constructor attraverso il tuo merge, poi verifica:

console.log({}.polluted);                 // undefined
console.log(Object.prototype.polluted);   // undefined

Un'asserzione in CI, costo quasi nullo, intercetta regressioni da refactoring successivi.

FAQ

Quali chiavi dovrebbe filtrare un merge?

Blocca insieme __proto__, constructor e prototype. Filtrare solo __proto__ è insufficiente, perché constructor.prototype raggiunge lo stesso prototipo.

Perché Object.create(null) ferma la scrittura?

Un oggetto senza prototipo non ha catena di prototipi, quindi __proto__ resta una chiave stringa ordinaria. Funziona come contenitore di dati, ma non sostituisce il filtro delle chiavi né gli aggiornamenti delle dipendenze.

Come testo l'inquinamento del prototipo in CI?

Invia un payload che contiene __proto__ e constructor attraverso il tuo merge, poi verifica che {}.polluted e Object.prototype.polluted siano undefined. Un'asserzione costa quasi nulla e intercetta regressioni da refactoring successivi.

Ulteriori letture