JSON Toolkit

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.

Avanzado~5 min de lecturaPublicado el 2026-10-30Actualizado el 2026-10-30
Cómo funciona la contaminación de prototipos y cómo bloquearla

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.

Lectura adicional