How to Prevent Prototype Poisoning in JSON

Prototype poisoning, often referred to as prototype pollution, occurs when untrusted JSON input injects malicious properties into a JavaScript application's base object prototype, potentially leading to denial of service, logic bypasses, or remote code execution. Safeguarding web applications against this vulnerability requires securing both the data ingestion and data manipulation phases. This article outlines the primary security mechanisms used to mitigate prototype poisoning when parsing and processing JSON data, including reviver functions, prototype-free data structures, strict schema validation, and runtime hardening techniques.

Use a Reviver Function with JSON.parse()

By default, native JSON.parse() creates plain JavaScript objects inheriting from Object.prototype. If the parsed string contains keys such as __proto__, constructor, or prototype, standard property access or subsequent merge operations might modify the global prototype.

You can intercept and neutralize these dangerous keys directly during parsing by supplying a custom reviver function:

function safeReviver(key, value) {
  if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
    return undefined; // Strips the dangerous property
  }
  return value;
}

const safeData = JSON.parse(untrustedJsonString, safeReviver);

Store Data in Prototype-Free Objects or Maps

When constructing lookup tables, caches, or dictionaries from parsed JSON, avoid using standard object literals ({}). Instead, instantiate objects that do not inherit from Object.prototype:

  • Object.create(null): Creates a pure dictionary with no prototype chain, making it completely immune to __proto__ injection.
  • Map instances: Provide a key-value store where keys are treated purely as identifiers rather than object properties, preventing any prototype traversal.
const safeDictionary = Object.assign(Object.create(null), safeData);

Enforce Strict Schema Validation

Never trust incoming JSON payloads without structural validation. Employing schema validators like Zod, Joi, or Ajv ensures that parsed payloads conform strictly to expected data shapes and data types.

Most modern schema validation libraries automatically strip or reject unrecognized properties. When configuring validators, enable strict mode to reject unknown properties explicitly (for instance, using .strict() in Zod or additionalProperties: false in JSON Schema).

Utilize Secure Object Merging Utilities

Prototype pollution rarely triggers via JSON.parse() alone; the vulnerability usually manifests when the parsed output is passed into a recursive "deep merge" or "extend" utility.

Vulnerable recursive functions copy properties indiscriminately:

// Vulnerable implementation pattern
target[key] = source[key];

To eliminate this vector:

  • Ensure all utility libraries (such as Lodash or Ramda) are regularly updated to versions with built-in prototype pollution defenses.
  • If writing custom clone or merge functions, explicitly guard against sensitive keys:
function safeDeepMerge(target, source) {
  for (const key of Object.keys(source)) {
    if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
      continue;
    }
    if (typeof source[key] === 'object' && source[key] !== null) {
      target[key] = safeDeepMerge(target[key] || {}, source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

Freeze the Object Prototype

At application startup, you can freeze standard built-in prototypes to make them immutable. This defense-in-depth measure prevents runtime modification of shared prototypes across the entire runtime environment:

Object.freeze(Object.prototype);
Object.freeze(Array.prototype);
Object.freeze(Function.prototype);

If an attacker manages to bypass input validation and attempts to write to a property on Object.prototype, the operation will fail silently in non-strict mode or throw a TypeError in strict mode. Note that this measure should be thoroughly tested, as some legacy third-party libraries modify prototypes legitimately.

Runtime Configuration Flags

For Node.js environments, runtime-level flags can restrict or disable prototype modification behaviors:

  • --disable-proto=delete: Disables Object.prototype.__proto__, converting it into a normal property rather than a prototype accessor.
  • --disable-proto=throw: Throws an exception whenever __proto__ is accessed or modified.
  • --frozen-intrinsics: Automatically freezes all built-in JavaScript objects and prototypes when the Node.js process initializes.