JavaScript is unique amongst mainstream programming languages in that it makes use of object-based inheritance. Rather
than being instantiated from classes, most objects are associative arrays that inherit properties from an existing
object (the prototype). Each object has a back-reference to the prototype object via the __proto__
property.
The presents an opportunity for attackers: if they can modify the prototype object, they can potentially inject code to all in-memory objects created in the same way.
Risks
If the attacker replaces a commonly called function on objects, they can execute whatever code they choose in that environment. This permits cross-site scripting attacks in the browser, or remote-code execution in Node.js applications.
How Prototype Pollution Works
Consider the following code taken from an early version of the express-fileupload
module in Node.js:
function processNested(data) {
if (!data || data.length < 1) return {}
let d = {}, keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
let key = keys[i],
value = data[key],
current = d,
keyParts = key
.replace(new RegExp(/\[/g), '.')
.replace(new RegExp(/\]/g), '')
.split('.')
for (let index = 0; index < keyParts.length; index++) {
let k = keyParts[index]
if (index >= keyParts.length - 1){
current[k] = value
} else {
if (!current[k]) current[k] = !isNaN(keyParts[index + 1]) ? [] : {}
current = current[k]
}
}
}
return d
}
This function is designed to “unfold” meta-data object like:
{
"a.b.c" : "value"
}
…into a nested object with the following form:
{
"a" : {
"b" : {
"c" : "value"
}
}
}
However, the code is way too permissive - it allows fields to be set on the built-in __proto__
object in the following
manner:
let payload = JSON.parse(
'{ "__proto__.injected" : "This variable exists on all objects" }'
)
processNested(payload)
// This will print "This variable exists on all objects", since
// we have injected a property in the global namespace.
console.log(injected)
// This will print "This variable exists on all objects", since
// all new objects will have this property too!
console.log(Object().injected)
Mitigation
To mitigate prototype pollution attacks, make sure you explicitly enumerate the properties you set on objects in
response to user actions. In particular, be sure not to set overwrite any internal properties that begin with the
_
character. When dealing with nested objects, make assertions in your code about the types of objects you
are dealing with as you pull them from properties.
There are a number of other coding practices that will help avoid prototype pollution vulnerabilities.
Freeze Your Objects
You can make your objects immutable using the freeze()
method:
const obj = {
prop: 42
}
// Make the object immutable.
Object.freeze(obj)
// The object has become frozen, so this will return true.
Object.isFrozen(obj)
// Attempts to modify the object throw an error.
obj.prop = 33
Freezing an object also prevents its prototype from being changed.
Use Prototypeless Object
JavaScript objects can be created without any prototype. Objects created from Object.create(null)
won’t have
__proto__
and constructor attributes. In this way, the object prototype will never be polluted.
Use Maps Instead of Objects
The Map
primitive was introduced in ES6. The Map
data structure stores key/value pairs, and it is not susceptible to
prototype pollution.
const map1 = new Map();
map1.set('a', 1);
map1.set('b', 2);
map1.set('c', 3);
// Will print "1"
console.log(map1.get('a'));
Further Considerations
Prototype pollution vulnerabilities often occur in third-party JavaScript libraries, so make sure to keep ahead of
security advisories periodically running the npm audit
tool if you are developing in Node.js.