Dock12/JavaScript prototype chain and security risks

Created by Luca Famà Fri, 03 Feb 2023 15:53:19 +0100 Modified Fri, 03 Feb 2023 15:53:19 +0100

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.

javascript_runtime_colored2.drawio.png

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).

show_prototype.png

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:

array_example_inherit.png

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).

array_proto.png

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:

myarray_proto_chain2.png

JavaScript allows to access the prototype of any object, using the __proto__ property:

user_proto.png

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:

tostring_not_defined.png

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 to null

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: