FlawOpen

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

Is Object.assign() safe from prototype pollution?

CWE-1321: Prototype PollutionReference page
Short answer

Not entirely. Object.assign() copies own enumerable source properties by assigning them to the target. If the source has an own enumerable __proto__ key, that assignment can invoke the inherited setter and change the target object’s prototype. A single shallow assign does not by itself modify the shared Object.prototype; recursive merge code can create that broader impact.

UNSAFE — assign and recursive merge
// 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] });
    }
  }
}
SAFE — spread for shallow copies; guarded recursive merge
// 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;
}

What Object.assign() does with the __proto__ key

Object.assign() copies each own enumerable source property to the target using the target’s ordinary property-setting behavior. When a JSON-parsed source has an own "__proto__" property and the target inherits the legacy setter, that setter changes the target’s [[Prototype]]. It does not add a literal own "__proto__" property in this case.

Object spread ({ ...source }) creates a data property for each copied property, so the same source key becomes an own data property on the result and does not call the inherited setter. This is a useful distinction for a shallow copy, not a general defense for later recursive merges.

Where recursive merges can pollute shared prototypes

Prototype pollution occurs when unsafe recursive code follows attacker-controlled keys through an object graph and writes onto a shared prototype. A common pattern reads target[key] to locate a nested object. If key is "__proto__", that read can return Object.prototype; a subsequent recursive write then affects ordinary objects globally. Other paths such as constructor.prototype also need consideration.

During review, ask which operations read or write into a target using attacker-controlled keys, whether those operations invoke setters, and whether every recursive level rejects dangerous keys.

FAQ

Is object spread ({ ...obj }) equally safe?

Yes for a shallow copy: spread creates an own data property and does not invoke the inherited setter. By contrast, Object.assign() can change the target object’s prototype when given an enumerable "__proto__" source key. Neither shallow operation alone modifies the shared Object.prototype.

What about structuredClone()?

structuredClone() creates a separate clone and does not apply the source object’s prototype chain. An own "__proto__" data key remains data in the clone; a later unsafe merge can still interpret that key specially.

Should I just use a library?

Use a maintained merge utility, keep it current, and verify its handling of "__proto__", "constructor", and "prototype" at every nesting level. Several widely used merge and object-path utilities have had prototype-pollution advisories; only fixed versions include those patches.

References

See also JavaScript Prototype Pollution: Causes & Fixes Does JSON.parse() cause prototype pollution? Is JSON.parse() safer than eval()? Is lodash.merge() safe from prototype pollution?