Need help with your JSON?
Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool
Protecting Against Prototype Pollution in JSON Parsing
In JavaScript and TypeScript, understanding how data is structured and processed is crucial for security. One significant vulnerability to be aware of, particularly when dealing with external data sources like JSON, is Prototype Pollution. While often discussed in the context of object merging or cloning, it can seem related to JSON parsing, and processing parsed JSON data is a common attack vector.
This article explores what Prototype Pollution is, its connection (or lack thereof) to standard JSON parsing, how it can manifest when handling parsed JSON, and, most importantly, how to protect your applications.
What is Prototype Pollution?
In JavaScript, objects inherit properties and methods from their prototype. If an attacker can inject properties into the prototype of a base object (like Object.prototype
), those injected properties will be accessible on *all* objects in the application, potentially leading to:
- Denial of Service (DoS) by crashing the application.
- Remote Code Execution (RCE) if coupled with other vulnerabilities (e.g., through gadget chains).
- Property manipulation or data leakage.
The core idea is exploiting insecure recursive merge, clone, or assignment functions that don't properly validate user-supplied keys.
The Role of JSON.parse()
Let's clarify a common point of confusion: The standard JSON.parse()
function in modern JavaScript engines is NOT vulnerable to Prototype Pollution directly.
JSON.parse()
is designed to create plain objects (created with {}
) and arrays (created with []
) from a JSON string. When it encounters keys like"__proto__"
, "constructor"
, or "prototype"
, it treats them simply as property names, just like any other string key. It does not traverse the prototype chain or modify the prototype of the object it's creating.
Safe Parsing Example:
const maliciousJson = '{ "user": "admin", "__proto__": { "isAdmin": true } }'; const parsedData = JSON.parse(maliciousJson); console.log(parsedData.user); // Output: admin console.log(parsedData.__proto__); // Output: { isAdmin: true } (a regular property) // This does NOT affect the prototype of other objects: const innocentObj = {}; console.log(innocentObj.isAdmin); // Output: undefined
JSON.parse()
correctly interprets "__proto__"
as a literal key name, not a special property accessor.
So, parsing untrusted JSON data using just JSON.parse()
does not inherently expose you to Prototype Pollution.
Where the Danger Lies: Processing Parsed JSON
The vulnerability typically arises after the JSON has been parsed into a JavaScript object, when that object is then used in operations that recursively traverse and assign properties, such as:
- Deep merging user-supplied data into a configuration object.
- Cloning objects where the clone function doesn't handle prototype properties correctly.
- Assigning properties recursively from a source object to a target object.
A common pattern where this occurs is a naive "deep extend" or "recursive merge" function, often implemented to update settings or objects with nested structures.
Example of a VULNERABLE Deep Merge Function:
// THIS IS VULNERABLE - DO NOT USE IN PRODUCTION WITHOUT SANITIZATION function vulnerableDeepMerge(target: any, source: any): any { for (const key in source) { // Problem: It iterates over inherited properties too unless hasOwnProperty check is missing (which it often is in vulnerable code) // Problem: It doesn't check for "__proto__", "constructor", "prototype" keys if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { // Recursively merge nested objects target[key] = vulnerableDeepMerge(target[key] || {}, source[key]); } else { // Assign primitive or array values target[key] = source[key]; } } return target; } // --- Attack Scenario --- const baseConfig = { app: { name: "MyApp", version: "1.0" }, settings: { theme: "dark" } }; const userInputJson = `{ "app": { "version": "1.1" }, "settings": { "__proto__": { "polluted": "PWNED!" } } }`; const parsedUserInput = JSON.parse(userInputJson); console.log("Before merge:", ({} as any).polluted); // Output: undefined // Attacker controls parsedUserInput, passes it to vulnerableDeepMerge vulnerableDeepMerge(baseConfig, parsedUserInput); console.log("After merge:", ({} as any).polluted); // If vulnerable, Output: PWNED! console.log("Merged Config:", baseConfig); // Shows the regular merge happened // Now any plain object might inherit the polluted property const anotherObj = {}; console.log("New object polluted?", (anotherObj as any).polluted); // If vulnerable, Output: PWNED!
The vulnerability occurs because the merge function doesn't sanitize the keys from the parsed JSON before assigning them, allowing "__proto__"
to be treated as a key pointing to the prototype.
How to Protect Against Prototype Pollution
Protection involves sanitizing or validating data *before* using it in potentially vulnerable operations like deep merges or recursive assignments. Here are the key strategies:
1. Always Use `JSON.parse()`
As established, JSON.parse()
itself is safe. Do not use insecure custom JSON parsers or eval-based methods.
Correct (Safe) Parsing:
// ALWAYS use JSON.parse() for untrusted JSON strings const parsedUserData = JSON.parse(untrustedJsonString);
2. Sanitize Input Before Processing
If you need to use parsed JSON data in operations like merging or extending, validate the keys in the parsed data. Reject or filter out keys that could target the prototype.
Sanitization Example (Recursive Filter):
function isObject(item: any): item is object { return (item && typeof item === 'object' && !Array.isArray(item) && item !== null); } function sanitizeObject(obj: any): any { if (!isObject(obj)) { return obj; // Return non-objects directly } const sanitized: { [key: string]: any } = {}; for (const key in obj) { // IMPORTANT: Check if the key is potentially malicious if (key === "__proto__" || key === "constructor" || key === "prototype") { console.warn(`Potential prototype pollution attempt blocked: key "${key}"`); continue; // Skip malicious keys } // OPTIONAL: Check if the property is directly on the object, not inherited (good practice) if (Object.prototype.hasOwnProperty.call(obj, key)) { // Recursively sanitize nested objects if (isObject(obj[key])) { sanitized[key] = sanitizeObject(obj[key]); } else if (Array.isArray(obj[key])) { // Sanitize array elements if they are objects sanitized[key] = obj[key].map(sanitizeObject); } else { sanitized[key] = obj[key]; } } } return sanitized; } // --- Safe Usage Scenario --- const baseConfig = { /* ... */ }; const userInputJson = `{ /* ... malicious payload ... */ }`; const parsedUserInput = JSON.parse(userInputJson); const sanitizedUserInput = sanitizeObject(parsedUserInput); // Now merge the SANITIZED data // Use a SAFE merge function (or the sanitized data with a regular merge) // Example: Object.assign({}, baseConfig, sanitizedUserInput); // Simple merge if structure is flat enough // For deep merge, ensure the deep merge function ITSELF checks keys or use the sanitized data // Using the vulnerableDeepMerge *with sanitized data* becomes safer: // vulnerableDeepMerge(baseConfig, sanitizedUserInput); // Now __proto__ is filtered out
This function recursively removes keys that target the prototype from the parsed JSON object before it's used in operations like merging.
3. Use Safe Object Manipulation Libraries/Functions
Reliable libraries often have built-in protections against Prototype Pollution in their merge, clone, or set functions.
- Libraries like Lodash and jQuery had past vulnerabilities but have been updated to protect against Prototype Pollution in functions like
_.merge()
or_.extend()
. Always use recent versions. - When implementing your own object manipulation logic, ensure it explicitly avoids or checks for keys like
"__proto__"
,"constructor"
, and"prototype"
. - Use
Object.prototype.hasOwnProperty.call(obj, key)
instead of justobj.hasOwnProperty(key)
when iterating over object properties to avoid issues ifhasOwnProperty
itself is polluted. - Consider using
Object.keys()
orObject.getOwnPropertyNames()
to iterate only over own properties, although this alone might not protect against__proto__
if assigned directly as a regular property name. The explicit key check is the most robust defense.
Example of a Safer Merge Logic Snippet:
function saferMerge(target: any, source: any): any { if (!isObject(source)) { return source; // Only merge objects } if (!isObject(target)) { // If target is not an object, replace it with a new object to merge into target = {}; } for (const key in source) { // Critical Check 1: Skip inherited properties if (!Object.prototype.hasOwnProperty.call(source, key)) { continue; } // Critical Check 2: Explicitly check for malicious keys if (key === "__proto__" || key === "constructor" || key === "prototype") { console.warn(`Prototype pollution attempt blocked during merge: key "${key}"`); continue; // Skip malicious keys } // Recursive merge for nested objects if (isObject(source[key])) { target[key] = saferMerge(target[key], source[key]); // Recursively call saferMerge } else { // Assign other values target[key] = source[key]; } } return target; } // --- Safe Usage --- const baseConfig = { /* ... */ }; const userInputJson = `{ /* ... malicious payload ... */ }`; const parsedUserInput = JSON.parse(userInputJson); // Still safe here // Use the safer merge function with parsed data const mergedConfig = saferMerge({...baseConfig}, parsedUserInput); // Clone baseConfig first if you want immutability console.log("Merged Config (safe):", mergedConfig); console.log("New object polluted?", ({} as any).polluted); // Should still be undefined
This version adds checks for own properties and explicitly skips the problematic keys.
4. Consider Freezing Object.prototype (Advanced/Caution)
In some environments (e.g., certain server-side Node.js applications where you have control over the startup), you might consider freezing Object.prototype
to prevent any additions or modifications.
Example:
// Caution: This must be done early in your application's lifecycle. // It might break poorly written libraries that expect to modify prototypes. // Object.freeze(Object.prototype); // console.log(Object.isFrozen(Object.prototype)); // Output: true
Freezing the prototype is a strong defense but can have compatibility issues. Use with caution and thorough testing.
Key Takeaways
- Standard
JSON.parse()
is safe against Prototype Pollution. - Vulnerabilities arise when processing the parsed JSON using functions (like deep merge/clone) that don't properly validate or sanitize property names.
- Malicious payloads use keys like
"__proto__"
,"constructor"
, or"prototype"
to inject properties into base object prototypes. - Protect yourself by:
- Always using
JSON.parse()
. - Sanitizing parsed data by filtering out malicious keys before using it in recursive operations.
- Using modern, well-maintained libraries for object manipulation (like merging) that have built-in protections.
- Implementing your own object manipulation logic with explicit checks for malicious keys.
- Always using
By understanding that the risk lies in the processing logic rather than the parsing itself, and by implementing robust checks during object manipulation, you can effectively protect your application against Prototype Pollution attacks originating from untrusted JSON inputs.
Need help with your JSON?
Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool