JavaScript Prototype Poisoning Vulnerabilities in the Wild

The Node.js Security Working Group was formed in early 2017 to help develop security policy and procedures for the Node.js project and ecosystem. One of the initiatives it has taken on is the reporting of vulnerabilities in third-party modules in the Node.js ecosystem. HackerOne is now the tool used for reporting and disclosing these vulnerabilities.

In the short time that this system has been in place, many vulnerabilities have been disclosed. Due to the sheer size of npm, an interesting effect is that a given type of vulnerability tends occurs in multiple similar packages. This article is the first of a series on particular vulnerabilities and attacks, where we’ll pick a class of vulnerabilities, go over what’s actually happening at a deep level, and explore how to mitigate the attack.

One vulnerability that has come up a lot is prototype poisoning by copying properties. Let’s dive deep into this type of vulnerability, and see how it can be prevented in one’s own projects, starting with a little background.

(Note: This article assumes you have a basic grasp of JavaScript prototypes. If this topic is unfamiliar to you, MDN describes the concept quite well!)

__proto__

JavaScript supports accessing the prototype of an object through the __proto__ property, which is a deprecated feature of EcmaScript. Properties can be retrieved or assigned on a prototype by accessing properties of the __proto__.

The __proto__ property can be thought of as having been defined for convenience like this:

Object.defineProperty(Object.prototype, '__proto__', {
get () {
return Object.getPrototypeOf(this);
},
set (newProto) {
Object.setPrototypeOf(this, newProto);
}
});

Example:

const myProto = { a: 1 };
const myObj = { b: 2 };
Object.setPrototypeOf(myObj, myProto);
// Now the prototype of myObj is myProto, and we can see that, since
// the log messages here are "true"
console.log(myObj.__proto__ === myProto);
console.log(Object.getPrototypeOf(myObj) === myProto);
// Now let's modify the prototype via __proto__
myObj.__proto__.c = 3;
console.log(myObj.c === 3); // true
console.log(myProto.c === 3); // true
console.log(myObj.__proto__.c === 3); // true

Recall that Object.prototype is the default prototype for new JavaScript objects defined with the literal syntax.

JSON.parse's quirk with __proto__

JSON.parse(), when passed properly formed JSON, will always produce plain JavaScript objects with Object.prototype as their prototype, even in the depths of a deeply nested object.

This means that even if __proto__ appears in the JSON, this will produce a new property on the object called __proto__ rather than setting the object's prototype, as it ordinarily would in JavaScript.

Example:

const plainObj = {
__proto__: { a: 1 },
b: 2
};
const jsonString = `{
"__proto__": { "a": 1 },
"b": 2
}`;
const parsedObj = JSON.parse(jsonString);
// The plain object's prototype is what was assigned inline.
console.log(plainObj.__proto__.a === 1); // true
const plainObjProto = Object.getPrototypeOf(plainObj);
console.log(plainObjProto !== Object.prototype); // true
console.log(plainObjProto.a === 1); // true
// The parsed object has a property called __proto__, overriding
// the __proto__ getter and setter, but its prototype is still
// Object.prototype.
console.log(parsedObj.__proto__.a === 1); // true
const parsedObjProto = Object.getPrototypeOf(parsedObj);
console.log(parsedObjProto !== Object.prototype); // false
console.log(parsedObjProto.a === 1); // false

Putting them together to make an attack vector

Consider a situation where user input is copied on to an object. An obvious use case for this might be updating user data. An app might take an object full of properties from a web interface, and apply them to an existing user object. A deep-copying library might be used to copy those properties from the supplied data to the session user object.

// Assume express, with some middlewares to populate the session
// object and grab params.
app.put('/update-user', (req, res) => {
copy(req.session.user, req.params.user);
res.send('ok');
});

Most deep copying libraries iterate over an object’s own properties, copy the primitives over, and recurse into the own properties that are objects.

Now consider some malicious JSON data sent to this endpoint.

{
"user": {
"__proto__": {
"admin": true
}
}
}

If this JSON is sent, JSON.parse will produce an object with a __proto__ property. If the copying library works as described above, it will copy the admin property onto the prototype of req.session.user! This could be disastrous if that property is used by the application. If the prototype of req.session.user is Object.prototype, as some database connectors might do, then this admin property would now be visible on all objects!

This is a contrived example, but any time user data is passed from JSON.parse to a vulnerable deep-copying library, this could be an attack vector.

Mitigation

The libraries that were affected by this problem, and were reported via HackerOne, have since made changes so that own properties named __proto__ are now ignored. This solved this most egregious case, but data coming in from users should always be filtered and sanitized.

Here is the list of modules that have been reported and then fixed for this type of attack (linking to their HackerOne reports). At the time of writing, the following modules were downloaded a staggering 29,055,731 times this past week alone.

Thank you HoLyVieR for reporting all of these!

If you’re using a deep-copying library that isn’t listed here, be sure to check the usual vulnerability lists (like Snyk, NSP, and the Node.js Security WG vulns list). If it isn’t listed, it may already have been patched for this vulnerability anyway, so check the source code to see if it filters out the __proto__ property.

If you’re doing your own deep-copying, or are an auther of a deep-copying library, be sure to filter out the __proto__ property!

Here’s a trivial example of a deep-copying function that protects against prototype poisoning:

// WARNING Do not actually use this function. It's just an example 
// to illustrate mitigating the attack.
function deepCopy(obj) {
const copied = Array.isArray(obj) ? [] : {};
for (const [key, value] of Object.entries(obj)) {
switch (typeof value) {
case 'number':
case 'string':
case 'symbol':
case 'boolean':
case 'undefined':
copied[key] = value;
break;
case 'function':
// In the interest of simplicity, functions are excluded
// from this trivial example.
break;
case 'object':
// BEGIN prototype poisoning mitigation
if (key === '__proto__') {
break;
}
// END prototype poisoning mitigation
copied[key] = deepCopy(value);
}
}
return copied;
}

Note that we’re only blocking the setting of __proto__ in the event that it's an object, since that's the only case in which it will recurse.

Sometimes features in JavaScript that are rarely used, or are deprecated, can be used to trick your code into doing unexpected and undesirable things. Stay vigilant!

This article was written by me, Bryan English. I work at a company called Intrinsic where we specialize in writing software for securing Node.js applications using the Least Privilege model. Our product is very powerful and is easy to implement. If you are looking for a way to secure your Node applications, give us a shout at hello@intrinsic.com.