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.Mapinstances: 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: DisablesObject.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.