CodePark/ questions / Input validation & safe defaults
START PRACTICINGPRACTICE
Backend question · Auth & security › Input validation & safe defaults

What does the last line do, and how do you defend against it?

function merge(target, source) {
  for (const key in source) {
    if (typeof source[key] === "object" && source[key] !== null) {
      target[key] = merge(target[key] || {}, source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

merge({}, JSON.parse('{"__proto__": {"isAdmin": true}}'));

The last line gives every object in the process isAdmin: true. This is prototype pollution.

How it happens:

  • JSON.parse creates a normal own property named __proto__.
  • The recursive merge then writes into target.__proto__, which is Object.prototype.
  • Every object inherits from Object.prototype, so every object now has isAdmin: true.

So a check like if (user.isAdmin) passes for any user who has no isAdmin field. Depending on the code, this can lead to authorization bypass, crashes, or even RCE (remote code execution) through polluted options.

Defences:

  • Skip the keys __proto__, constructor and prototype in merge, clone and path-setting code.
  • Keep libraries that do deep merges up to date.
  • Validate request bodies with strict schemas that reject unknown keys.
  • Use Map or Object.create(null) for dictionaries with user-supplied keys. These have no prototype to pollute.
  • Check own properties with Object.hasOwn.
  • A __proto__ key in a merge writes to Object.prototype.
  • Every object then inherits the polluted property.
  • The impact can be auth bypass, DoS (denial of service) and sometimes RCE.
  • Block the __proto__, constructor and prototype keys in merges.
  • Use strict schemas, Map or null-prototype objects.