flawopen.com/Prototype Pollution/Object.assign()
Is Object.assign() safe from prototype pollution?
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.
// 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] }); } } }
// 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
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.
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.
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.