FlawOpen

flawopen.com/Prototype Pollution/Object.assign()

¿Object.assign() evita Prototype Pollution?

CWE-1321: Prototype PollutionPágina de referencia
Respuesta breve

No del todo. Object.assign() copia las propiedades propias enumerables asignándolas al destino. Si el origen tiene una clave propia enumerable __proto__, la asignación puede invocar el setter heredado y cambiar el prototipo del objeto destino. Una asignación superficial no modifica por sí sola el Object.prototype compartido; una fusión recursiva sí puede ampliar el impacto.

INSEGURO — assign y fusión recursiva
// A JSON own "__proto__" key reaches the inherited setter
const source = JSON.parse('{"__proto__":{"isAdmin":true}}');
const target = Object.assign({}, source);
// target's prototype changes; Object.prototype does not.

// Recursive merge can reach Object.prototype
function deepAssign(target, source) {
  for (const key of Object.keys(source)) {
    if (source[key] && typeof source[key] === 'object') {
      target[key] = target[key] || {};
      deepAssign(target[key], source[key]);
      // With key === '__proto__', target[key] can be
      // Object.prototype; recursion then writes into it.
    } else {
      Object.assign(target, { [key]: source[key] });
    }
  }
}
SEGURO — spread superficial y fusión recursiva protegida
// Spread creates an own data property; it does not invoke the setter.
const shallow = { ...userInput };

// Reject dangerous keys at every depth and use own data properties.
const blocked = new Set(['__proto__', 'constructor', 'prototype']);
function isRecord(value) {
  if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
  const proto = Object.getPrototypeOf(value);
  return proto === Object.prototype || proto === null;
}
function copySafe(value) {
  if (Array.isArray(value)) return value.map(copySafe);
  return isRecord(value) ? mergeSafe(value) : value;
}
function mergeSafe(source, target = Object.create(null)) {
  if (!isRecord(source)) throw new TypeError('Expected a plain object');
  for (const key of Object.keys(source)) {
    if (blocked.has(key)) continue;
    const value = source[key];
    const current = Object.hasOwn(target, key) ? target[key] : undefined;
    const next = isRecord(value)
      ? mergeSafe(value, isRecord(current) ? current : Object.create(null))
      : copySafe(value);
    Object.defineProperty(target, key, {
      value: next, enumerable: true, writable: true, configurable: true
    });
  }
  return target;
}

Qué hace Object.assign() con la clave __proto__

Object.assign() copia cada propiedad propia enumerable del origen mediante el comportamiento normal de asignación del destino. Si el origen analizado desde JSON tiene una propiedad propia __proto__ y el destino hereda el setter antiguo, ese setter cambia el [[Prototype]] del destino. En este caso no se crea una propiedad propia literal __proto__.

El spread de objetos ({ ...source }) crea propiedades de datos en el resultado; la misma clave queda como propiedad propia y no llama al setter heredado. Esta diferencia sirve para copias superficiales, pero no protege fusiones recursivas posteriores.

Cómo las fusiones recursivas pueden contaminar prototipos compartidos

Prototype Pollution ocurre cuando código recursivo inseguro sigue claves controladas por el atacante y escribe en un prototipo compartido. Un patrón común lee target[key] para encontrar un objeto anidado. Si key es __proto__, esa lectura puede devolver Object.prototype; la siguiente recursión puede afectar globalmente a objetos normales. También hay que revisar rutas como constructor.prototype.

En la revisión, busca lecturas o escrituras en el destino con claves controladas por el atacante, comprueba si activan setters y verifica que cada nivel recursivo rechace claves peligrosas.

Preguntas frecuentes

¿El spread de objetos ({ ...obj }) es igual de seguro?

Sí, para una copia superficial: spread crea una propiedad de datos propia y no invoca el setter heredado. En cambio, Object.assign() puede cambiar el prototipo del destino si recibe una clave enumerable __proto__. Ninguna de las dos operaciones superficiales modifica por sí sola el Object.prototype compartido.

¿Y structuredClone()?

structuredClone() crea una copia separada y no aplica la cadena de prototipos del origen. Una clave de datos propia __proto__ permanece como dato en la copia; una fusión insegura posterior aún puede interpretarla de forma especial.

¿Basta con usar una biblioteca?

Usa una biblioteca de fusión mantenida y actualizada; comprueba cómo gestiona __proto__, constructor y prototype en todos los niveles. Varias utilidades populares han tenido avisos de Prototype Pollution; solo las versiones corregidas incluyen la protección.

Referencias

Véase también Contaminación de prototipos JS: causas y solución ¿JSON.parse() causa contaminación de prototipos? ¿JSON.parse() es más seguro que eval()? ¿lodash.merge() causa contaminación de prototipos?