JavaScript prototype chain and security risks
For the tenth year in a row, JavaScript is the most used programming language (according to StackOverflow survey). This should not be surprising, considering that JavaScript is the main programming language used (but non only) for web development, for both frontend and backend components (i.e Node.js or Deno).
Even though knowing JavaScript internals is not required to write programs, it's always useful to understand some implementation details of the language, especially from a security perspective.
In this article I'll focus on what is the prototype chain and which are the security implications associated with it. But let's start with a basic introduction about the JavaScript runtime environment.
JavaScript runtime environment
JavaScript is actually the most famous implementation of the ECMAScript specification.
In order to execute JavaScript code, a JavaScript runtime is required. A JavaScript runtime is the environment which includes all the required components to run JavaScript code.
This environment can be complex and it's not the main topic of the article, however the below picture can provide a basic (and simplified) understanding of how the main components of a JavaScript runtime interact with each others.
A very important component is the JavaScript engine. There are several implementation of JavaScript engines, which allow different browsers to properly run the same JavaScript code. The most famous engines are:
- V8 (used in Chrome and also in Node.js for server side programming)
- SpiderMonkey (used in Mozilla Firefox)
- JavaScriptCore (used by Safari)
JavaScript Object
Everything in JavaScript is either a primitive type or an object. Primitive types are:
- string
- number
- bigint
- boolean
- undefined
- symbol
- null
A JavaScript object is basically a collection of properties, each with a corresponding value (which can be an object as well) . For example the following code, initializes a new object user
with 2 properties (a username
property of type string and a isAdmin
property of type boolean).
const user = {
username: "John",
isAdmin: false
}
After object has been created, we can refer its properties using either the dot notation:
console.log(user.username); // will print "John" to the console
or the bracket notation:
console.log(user["username"]); // will print "John" to the console
Inheritance and prototype chain
JavaScript is a prototype-based language which is slightly different from a "classic" Object-oriented programming language (like Java).
In JavaScript each object has a private property which points to its prototype. This is true until we find an object with a null
prototype. This mechanism allows developer to navigate the prototype chain of any object.
For example, let's analyze our user
object (you can reproduce the same example just by opening the developer console in your current browser).
We can see that the prototype for our user
object (referred as [[Prototype]]
), is of type Object
.
This means that our user
object inherits any property that is defined in its prototype. We can see why this is really useful, by declaring a simple array and using the inherited toString
function to print all the array elements and the inherited length
property to print the number of elements:
If we look closely our myarray
object, we can see that its prototype is of type Array
, so it inherits a lot of properties from the Array
prototype (since the inherited properties for the Array prototype are too many, the output is truncated).
At the same type, the Array
prototype has a prototype itself (the Object
prototype) which has a prototype as well (the null
prototype). So we can represent the prototype chain of our myarray
object as shown below:
JavaScript allows to access the prototype of any object, using the __proto__
property:
Furthermore, we can even overwrite the __proto__
property. For example, here we are setting the user
prototype to null
which means that the object will lose the inherited properties from the Object
prototype. We can confirm by verifying that the toString
function is not defined anymore for this object:
It's important to notice that whenever we try to access an object property (i.e. user.isAdmin
or user["isAdmin"]
) the JavaScript engine will first try to find the property from the own object properties. If the property is not found, it will try to look for the same property from the object prototype. This process will continue until either there is a match or the null
prototype has been reached.
How attackers can abuse the prototype chain
Let's imagine we have a Node.js application which only allows administrative user to access some privileged functionalities. The check could be implemented as follow:
function isUserAdmin(userId){
let isAdmin = false;
const userPermission = checkUserPermissionDB(userId);
if(userPermission.isAdmin){
isAdmin = userPermission.isAdmin;
}
return isAdmin;
}
where checkUserPermissionDB
performs a database query and returns the user permissions as a JavaScript object.
Now, if the application includes some unsafe function which allows an attacker to overwrite prototype properties, the attacker can easily bypass the security check and access the admin functionalities.
For example, let's say that this application also includes the possibility to update some user data, like age, description, language and so on.
function updateUser(userId, requestBody){
const userObj = getUserFromDB(userId);
merge(userObj, requestBody);
saveUserToDB(userObj);
}
where the merge
function is defined as follow:
let isObject = function(a) {
return (!!a) && (a.constructor === Object);
}
function merge(target, source) {
for (var attr in source) {
if (isObject(target[attr]) && isObject(source[attr])) {
merge(target[attr], source[attr]);
} else {
target[attr] = source[attr];
}
}
return target;
}
This merge(target, source)
function is basically recursively merging all the properties from source
object to target
object. For example, if we have an object obj1
and an object obj2
defined as follows:
obj1 = {
"property1":{
"foo":"bar"
}
};
obj2 = {
"property1":{
"bar":"foo"
},
"property2": "something"
};
after calling merge(obj1, obj2)
, object obj1
will be:
obj1 = {
"property1":{
"foo":"bar",
"bar":"foo"
},
"property2": "something"
};
Let's assume that the user data is sent by the client via a POST
request, so a legit POST request body would look like this:
POST /api/updateUser
Host: somehost
Content-Type: application/json
....
{
"age":21,
"description": "Hi there!",
"language":"en"
}
What happens if an attacker will try to send the following payload instead?
POST /api/updateUser
Host: somehost
Content-Type: application/json
....
{
"age":21,
"description": "Hi there!",
"language":"en",
"__proto__":{
"isAdmin":true
}
}
As the attacker defined the __proto__
property as an object, this will trigger the vulnerable merge
function to copy all the properties defined by the attacker to the target
object prototype (which is the Object
prototype). This means that any object from now on, will inherit the property isAdmin:true
. In this specific scenario, any user (not only the attacker) becomes an admin user!
This type of attack is known as Prototype Pollution and it can lead to critical security incidents. It's worth to notice that similar attacks can happen client side (for example it can be used to trigger cross-site-scripting attacks).
How to prevent prototype pollution attacks
Merging (or cloning) objects without any prior validation can introduce prototype pollution vulnerabilities. More in general, any user input that is used to set nested properties should be carefully sanitized.
It might be really hard to write a robust and strong validation procedure, so the general advise is to always use well known and well tested open source libraries for this kind of tasks.
There are several additional methods for avoiding prototype pollution vulnerability, among which:
- use
Object.freeze(obj)
: this method "freezes"obj
so it's not possible anymore to overwrite its properties or to add new ones - create an object with
Object.create(null)
: the newly created object will not have any prototype - manually set the
__proto__
property tonull
If you want to learn more about prototype pollution and read about few real vulnerabilities discovered "in the wild", please refers to the following resources: