PREVIEW

Prototype pollution

Exposing the default prototype by calling unsafe recursive functions with untrusted data as input.

Prototype pollution: the basics

What is prototype pollution?

Prototype pollution is an injection attack that targets JavaScript runtimes. With prototype pollution, an attacker might control the default values of an object's properties. This allows the attacker to tamper with the logic of the application and can also lead to denial of service or, in extreme cases, remote code execution.

After reading the above definition, there are probably at least a dozen questions that spring to mind. What does it actually mean to “override object attributes at runtime”? How can it affect the security of my application? And, most importantly, how do I protect my code against this attack?

About this lesson

Prototype pollution can be complex, so we will walk through it in three steps.

  1. You will use prototype pollution to compromise a vulnerable API
  2. You will learn more about JavaScript prototypes and how prototype pollution works
  3. And, you will learn how to fix and prevent prototype pollution in your applications

Did you know? Not an isolated case

Prototype pollution vulnerabilities have been found and fixed in many popular JavaScript libraries, including jQuery, lodash, express, minimist, hoek… and the list goes on. When a prototype pollution vulnerability was discovered in jQuery, jQuery was--at that time--being used in 74% of all websites. Talk about scary!

Prototype pollution in action

Let’s demonstrate how a prototype pollution attack may play out in the real world. A company called startup.io, which we might recognize from the other lessons, finally acquired users for its products. Obliged by .io in its name, startup.io decided to release an API that allows users to manage data the company holds via the app.

Unfortunately, stressed by looming deadlines and chased by ever-demanding stakeholders, startup.io engineers did a bad job of securing their API. They never had time to schedule a meeting with their AppSec team to help with the design and they later ignored all the issues reported by the security scanners. As a result, their API contains many bugs and vulnerabilities--one of which is prototype pollution.

This is obviously bad for startup.io--but good news for us because with it we can compromise their API. Let’s focus on two API endpoints that startup.io exposes:

Our interactive widgets are optimized for larger screens. To access the complete Snyk Learn experience please switch to tablet or desktop.

Prototype pollution in action DO THIS

Vulnerable API

Let’s try to escalate our privileges to adminhood by tampering with the application logic. Then, let’s try to bring down the whole API with a denial of service attack.

All examples assume we are already authorized, and any authorization headers are omitted for readability. To interact with the API, we will be using an embedded terminal window like the one below.

Demo terminal

Prototype pollution under the hood

To understand why our attacks worked, we need to take a slight detour and explain what JavaScript prototypes are.

What is a prototype in JavaScript?

When we create an empty object in JavaScript (for example, const obj = {}), the created object already has many attributes and methods defined for it, for instance, the toString method. Have you ever wondered where all these attributes and methods come from? The answer is the prototype.

Many object-oriented languages, for example Java, use classes as blueprints for object creation. Each object belongs to a class, and classes are organized in parent-child hierarchies. When we call the toString method on an object, the language runtime will look for the toString method defined on the class a given object belongs to. If it cannot find such a definition, it will look for it in the parent class, then its parent class, until it hits the top of the class hierarchy.

JavaScript, instead, is a prototype-based object-oriented programming language. Each object is linked to a “prototype”. When we invoke the toString method on an object, JavaScript will first check to see if we explicitly defined the method for the given object. If we haven’t, it will look for its definition on the object’s prototype.

Just a normal JavaScript object DO THIS

Under the hood, a prototype of an object is just another JavaScript object and is publicly accessible via the __proto__ attribute. Run the code sample on the right:

Missing attributes come from the prototype DO THIS

If we try to access an attribute that doesn’t exist on an object, JavaScript will look into the object’s prototype to get the value for that attribute. To see this, run the code snippet. Even though a is an empty object and does not define someFunction explicitly, we can still call someFunction on a after we define the function on the prototype of a.

The shared default prototype DO THIS

So, where is the risk associated with prototypes? Well, here comes the critical part. Most objects share the same (default) prototype! For example, all objects created via the literal {} or the new Object() constructor will share the same prototype unless we explicitly override it. Consider our code snippet. a and b are unrelated objects, yet their __proto__ attributes point to the same object.

Setting attributes on a shared prototype DO THIS

What do you think will happen if we start setting or updating the attributes on a shared prototype object? Consider the code snippet. a and b have no direct connection but share the same prototype. As a result, changing the prototype of a affects b!

Prototype pollution explained

The bottom line is--if we modify a prototype shared by two or more objects, all objects will reflect this modification! They don’t even have to be in the same scope or otherwise related. And remember, most objects by default share the same prototype--so if we change the prototype of just one of these objects, we can change the behaviour of all of them!

What if a malicious person can change (or “pollute”) a prototype shared by multiple objects? In fact, this is what we did when we compromised startup.io’s API in the previous section. Remember, the payloads we sent to the server were:

{"about": {"__proto__":"{"role": "admin"}}}

{"about": {"__proto__": {"toString": "Two bytes meet. The first byte asks: Are you ill? The second byte replies: No, just feeling a bit off."}}}

We polluted the role and toString attributes of the default shared prototype object by sending a specially crafted HTTP POST request. To see how that attack worked, consider the code of the GET and POST HTTP request handlers:

A prototype pollution attack where a hacker sends malicious payload to the backend server, and an unsafe merge function recursively merges that payload with a backend object

Our interactive widgets are optimized for larger screens. To access the complete Snyk Learn experience please switch to tablet or desktop.

The updateUser method handles the HTTP POST request. The input requestBody is the payload we sent to the server.

Prototype pollution mitigation

Solution: Use safe open source libraries when recursively setting object's properties.

The merge function that startup.io wrote aimed to update one object with all attributes of another object. As we saw when we toured the code in the last section, the merge function is recursive and merrily merges all properties from its second input--even when it contains untrusted data with dubious keys such as __proto__.

Merging two objects is not the only functionality that can expose the code to a prototype pollution attack—any function which recursively sets nested properties can create an attack vector. Other common examples in the JavaScript ecosystem include: deep cloning (e.g. lodash cloneDeep), setting nested properties (e.g. lodash set), or creating objects by recursively "zipping" properties with values (e.g. lodash zipObjectDeep).

Always be sure to sanitize untrusted input when recursively setting nested properties. Don’t do this yourself! Even the best developers can easily get this wrong. Instead, use a library such as lodash, which is extremely popular and has excellent community support and a track record of promptly fixing security issues.

A prototype pollution mitigation, where a hacker tries to send a malicious input, but a safe merge function is used, preventing the malicious input from affecting the prototype

Our interactive widgets are optimized for larger screens. To access the complete Snyk Learn experience please switch to tablet or desktop.

To decide which libraries to trust, use Snyk Advisor! Snyk Advisor provides information on a given package's popularity, community support, and security. Also, check your open source libraries with vulnerability scanners such as Snyk, which will notify you about all new vulnerabilities discovered in any libraries you are using, and will help you mitigate them easily.

Did you know? It is hard to get right

Mitigating prototype pollution attacks is hard. While implementing a recursive merge function, lodash developers made sure a key with the value __proto__ would not be copied from one object to another. Unfortunately, it later turned out that prototype pollution is also possible through other properties, e.g. constructor.prototype (check this fix commit to learn how lodash developers dealt with that issue).

The lesson to learn here is that proper sanitization of user input is extremely hard. Whenever you can, use a battle-tested library to do the work for you.

Solution: Create objects without prototypes: Object.create(null)

Another way to avoid prototype pollution is to consider using the Object.create() method instead of the object literal {} or the object constructor new Object() when creating new objects. This way, we can set the prototype of the created object directly via the first argument passed to Object.create(). If we pass null, the created object will not have a prototype and therefore cannot be polluted.

Our interactive widgets are optimized for larger screens. To access the complete Snyk Learn experience please switch to tablet or desktop.

Solution: Prevent any changes to the prototype: use Object.freeze()

JavaScript comes with an Object.freeze() method, which we can use to prevent any changes to the attributes of an object. Since the prototype is just an object, we can freeze it, too. We can freeze the default prototype by invoking Object.freeze(Object.prototype), which prevents the default prototype from getting polluted.

Our interactive widgets are optimized for larger screens. To access the complete Snyk Learn experience please switch to tablet or desktop.

How do you mitigate prototype pollution?

To mitigate prototype pollution vulnerabilities in your codebase use popular open-source libraries when you need to recursively set nested properties on an object. Check which libraries to use with Snyk Advisor, and always make sure that the library you choose is free of vulnerabilities with scanners such as Snyk. To harden your code further, use Object.create(null) to avoid using prototypes altogether, or use Object.freeze(Object.prototype) to prevent any changes to the shared prototype.

Keep learning

To learn more about prototype pollution, check out our blog posts:

  • Read about how our security research team discovered prototype pollution in lodash and minimist.
  • Check out our coverage on prototype pollution findings in jQuery and express.

Finally, if you would like to dive deeper into prototype pollution, be sure to read this detailed report on prototype pollution written by Security Researcher, Olivier Arteau. He has found and responsibly disclosed many prototype pollution vulnerabilities in the most common JavaScript libraries.

Congratulations

You’ve learned what prototype pollution is and how to protect your applications from it. We hope you will apply your new knowledge wisely and make your code safer. Please rate how valuable this lesson was and provide feedback to make it better. Also, make sure to check out our lessons on other common vulnerabilities.

Try Snyk. Be Secure

Are you sure that you don't have this vulnerability in your codebase?

Quick Start - Start For Free Chevron Right icon
Snyk Learn - Try Snyk