Qué es la inyección de __proto__
En resumen: JSON.parse trata __proto__ como una clave normal; el daño ocurre en una fusión recursiva que escribe en Object.prototype, así que todos los objetos heredan propiedades del atacante.

En resumen: bloquea __proto__, constructor y prototype antes de cualquier fusión, fusiona entradas no confiables en Object.create(null) y añade una aserción de prototipo a CI; parchear las bibliotecas no basta.
En una frase: una clave no confiable llamada __proto__ se encuentra con una fusión recursiva, y la fusión escribe en el prototipo.
Una reproducción
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
No se tocó ningún objeto de negocio. Se tocó Object.prototype, y ahora todos los objetos llevan isAdmin.
Por qué JSON.parse no tiene la culpa
const data = JSON.parse('{"__proto__": {"a": 1}}');
Object.getOwnPropertyNames(data); // [ '__proto__' ]
Object.getPrototypeOf(data) === Object.prototype; // true, untouched
__proto__ llega como una propiedad propia normal. El peligro está enteramente en el código que después fusiona, clona en profundidad o asigna por ruta: _.merge, algunos patrones de Object.assign e incontables ayudantes set(obj, path, value) escritos a mano.
Impacto
- Elusión de autorización:
if (user.isAdmin)pasa a ser incondicionalmente verdadero - Corrupción de datos: campos inesperados se serializan en registros almacenados
- Denegación de servicio: objetos grandes adosados al prototipo aumentan la memoria de forma permanente
- Escalada: combinado con motores de plantillas o ejecutores de comandos, esto ha llevado a ejecución remota de código
Cuatro defensas
Filtra las claves.
const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
Bloquear solo __proto__ es insuficiente: constructor.prototype llega al mismo lugar.
Fusiona en un objeto sin prototipo.
const safeTarget = Object.create(null);
Sin cadena de prototipos, __proto__ es solo una clave de cadena.
Usa Object.hasOwn en lugar de in. in consulta la cadena de prototipos.
Mantén las dependencias parcheadas. lodash, deepmerge y json-patch han publicado correcciones para esta clase de error. Vigila npm audit.
Verifica que sigue corregido
Pasa una carga útil que contenga __proto__ y constructor por tu fusión y luego comprueba:
console.log({}.polluted); // undefined
console.log(Object.prototype.polluted); // undefined
Una aserción en CI, coste casi nulo, atrapa regresiones de refactorizaciones posteriores.
FAQ
¿Qué claves debería filtrar una fusión?
Bloquea __proto__, constructor y prototype juntas. Filtrar solo __proto__ es insuficiente, porque constructor.prototype llega al mismo prototipo.
¿Por qué detiene la escritura Object.create(null)?
Un objeto sin prototipo no tiene cadena de prototipos, así que __proto__ sigue siendo una clave de cadena ordinaria. Funciona como contenedor de datos, pero no sustituye al filtrado de claves ni a la actualización de dependencias.
¿Cómo pruebo la contaminación de prototipos en CI?
Envía una carga útil que contenga __proto__ y constructor por tu fusión y luego comprueba que {}.polluted y Object.prototype.polluted son undefined. Una aserción cuesta casi nada y atrapa regresiones de refactorizaciones posteriores.