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.