Security Resources

⌘K
  1. Home
  2. Security Resources
  3. Secure Code Explain
  4. Prototype Pollution

Prototype Pollution

Here is an example of JavaScript code that is vulnerable to prototype pollution:

🥺 Vulnerable Code

// Vulnerable: a recursive merge that copies any key, including __proto__
function merge(target, source) {
  for (const key of Object.keys(source)) {
    if (typeof source[key] === "object" && source[key] !== null) {
      target[key] = merge(target[key] || {}, source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

app.post("/api/settings", (req, res) => {
  const settings = merge(loadDefaults(), req.body);
  res.json(saveSettings(req.user.id, settings));
});

A body of {"__proto__":{"isAdmin":true}} walks up to Object.prototype and writes there, so from that moment every plain object in the process reports isAdmin === true. Depending on what else reads properties off objects - template engines, option parsers, child process helpers - the same primitive escalates from an authorization bypass to remote code execution.

😎 Secure Code

Here is a version of the same code that is secured against prototype pollution:

const BLOCKED_KEYS = new Set(["__proto__", "constructor", "prototype"]);

function merge(target, source) {
  for (const key of Object.keys(source)) {
    if (BLOCKED_KEYS.has(key)) continue;

    const value = source[key];
    if (value && typeof value === "object" && !Array.isArray(value)) {
      target[key] = merge(Object.create(null), value);
    } else {
      target[key] = value;
    }
  }
  return target;
}

app.post("/api/settings", (req, res) => {
  const settings = merge(Object.create(null), validateSettings(req.body));
  res.json(saveSettings(req.user.id, settings));
});

Dangerous keys are dropped, and the merge target is a null-prototype object so there is no prototype chain to poison in the first place. Validating the body against a schema before it reaches the merge is the stronger control, since it rejects unknown keys outright. Calling Object.freeze(Object.prototype) during startup and keeping merge and clone libraries patched are cheap defence in depth.