DEV Community

Cover image for Exploits Ep - 1: From Prototype Pollution to a 100% Discount
Adnan Hashmi for Middleware

Posted on • Updated on • Originally published at middlewarehq.com

Exploits Ep - 1: From Prototype Pollution to a 100% Discount

Imagine this: You're browsing your favourite online shop, adding those must-have items to your cart, when suddenly, a hacker decides to crash your shopping party. But instead of stealing your credit card info or adding a thousand rubber ducks to your order – they pollute your shopping cart's prototype! Sounds like a weird sci-fi plot, right? Well, welcome to the world of prototype pollution, where hackers can turn your JavaScript objects into their personal playground. In this blog post, we're going to dive into prototype pollution, using a vulnerable e-commerce site as our guinea pig. So, grab your hacker hoodies, and let's explore how a simple shopping cart can turn into a hacker's paradise!

cat typing

P.S.: After you've become a master at breaking JS, try out your skills on our repo, and maybe give us a star?

🌟 Star us on Github! 🌟

But before we HACK, here is a small refresher on javascript objects and prototypes.

What are Prototypes in Javascript

Almost everything in Javascript is an object and every object has a built-in property called prototype. The prototype itself is an object and serves as a fallback source for properties and methods. What that means is, when, javascript runtime doesn't find the property in the object, it checks for the property in the prototype of the object. If we want a shared behaviour between multiple objects we define that behaviour in the prototype of the constructor function instead of defining that behaviour in all the objects. Here is an example:

function Product(name, price) {
  this.name = name;
  this.price = price;
}

Product.prototype.display = function () {
  console.log(`${this.name} -> ${this.price}`);
};

let product_one = new Product("headphones", 1000);
let product_two = new Product("Mic", 400);

product_one.display();
product_two.display();

Enter fullscreen mode Exit fullscreen mode

In the above example, the objects product_one and product_two are derived from the function constructor Product. When we call the display() function on the objects the javascript runtime first checks the object if they contain the method called display. When it doesn't find it in the object itself, the runtime checks for this method in the prototype (__proto__) object. The __proto__ property of an object links it to the prototype property of its function constructor. So Product.prototype === product_one.__proto__.

This can also be checked by logging the products and checking their prototype object.

Logging objects in console

We see that the object product_one itself doesn't contain a method called display but its Prototype contains it. You may also notice that there is another Prototype nested inside the prototype of product_one. This is where things get a little interesting and its precisely this feature that opens doors to potential exploits. Let's explore this next!

Prototype Chaining and Prototype Pollution

Inheritance in javascript is implemented using objects. Each object has an internal link to a prototype object which has a prototype of its own and so on until an object is reached with null as its prototype.

Prototype chaining figure

The nested prototype you're seeing is actually the prototype of Object, which sits at the top of the prototype chain for most JavaScript objects. This means that if a property isn't found on product_one or its immediate prototype, JavaScript will continue searching up the chain, eventually reaching Object.prototype.

Prototype chain

For example, when we run product_one.hasOwnProperty("name"), JavaScript follows this lookup chain:

  1. First, it looks for hasOwnProperty in the product_one object itself. It doesn't find it there.
  2. Next, it checks product_one.__proto__, which points to Product.prototype. The hasOwnProperty method is not defined here either.
  3. Then, it moves up the prototype chain to Product.prototype.__proto__, which is equivalent to Object.prototype. Here, it finally finds the hasOwnProperty method.

So if we were to somehow manipulate the behaviour of Object.prototype we will be able to control the behaviour of all the objects linked to Object.prototype in the chain. This is called Prototype Pollution. Lets see this with our previous example of Product.

Code

This code is a classic example of prototype pollution. The first line modifies the prototype chain by adding a new property new_property to the Object.prototype. By setting new_property on this high-level prototype, the change affects all objects that inherit from Object.prototype. The second part of the code creates a new empty object obj and then attempts to access new_property on it. Despite obj not having this property directly defined, it still outputs "polluted" because the property is inherited from the polluted Object.prototype. This showcases how prototype pollution can unexpectedly affect seemingly unrelated objects, potentially leading to security vulnerabilities or unintended behaviour in an application.

Alright, enough theory. Lets get hacking!

Lets Get Hacking

Our favourite E-commerce site has a special offer for us of 100% discount🤯. But unfortunately we don't have the coupon code🥹. Wouldn't it be great if we could somehow snag that code? Maybe it's hiding in plain sight, hardcoded in the website's source.

Approach 1: Naive Approach

We open our trusty developer tools in the browser and go on to inspecting the code for the page. We move to the scripts section and check if we can find the coupon code.

Code

We see that there is a DISCOUNT_COUPON_HASH and a hashing function called hashValue. If we scroll down further we find the applyCoupon function. This function gets the value of the discount code text box, hashes it using the hashValue function and compares it with DISCOUNT_COUPON_HASH. Unfortunately, by the nature of hashing function the hash value is irreversible. So we can in no way get the value of coupon code that hashes to DISCOUNT_COUPON_HASH.

Screenshot

Let's explore this site further as our previous approach didn't bear any fruit.

Approach 2: Trying to craft a malicious URL with __proto__ query parameter

The shop has another very handy feature to store the cart state in the URL. This way we can share our cart with others or save the URL to come back to our cart. Prototype pollution attacks usually exploit how these query parameters are being parsed by crafting malicious urls. Let's try to craft a malicious URL to pollute Object.Prototype.

https://exploit-episode-1.middlewarehq.com/?cart={"items":{"2":3,"3":1}}&__proto__.hack=hacked
Enter fullscreen mode Exit fullscreen mode
https://exploit-episode-1.middlewarehq.com/?cart={"items":{"2":3,"3":1}}&__proto__[hack]=hacked
Enter fullscreen mode Exit fullscreen mode

Using the above URLs we try to inject the property hack:'hacked' in to the global object's prototype with the assumption that the URL parsing done to restore the cart doesn't sanitise the input. But the above urls don't seem to work as expected:

Screenshot

We need to explore the code some more to understand how the URL parsing is being done.

Investigating the code some more

Upon reading through the code we find loadCartFromURL function that is responsible for restoring the cart from URL on page load.

Screenshot

It creates a URLSearchParams object from the query parameters, gets the 'cart' query parameter and parses it to json using JSON.parse. After the object is parsed as json, the merge function merges the cart object and the updateObj recursively. Now this looks like something asking to be misused.

JSON.parse treats every key in the object as an arbitrary string, include __proto__. So now, instead of creating a new query parameter of __proto__ we will inject it into the cart object itself.

Approach 3: Injecting __proto__ into the cart query parameter

Let's try to use the following url:

https://exploit-episode-1.middlewarehq.com/?cart={"items":{"2":3,"3":1},"__proto__":{"hack":"hacked"}}
Enter fullscreen mode Exit fullscreen mode

Screenshot

Screenshot

And voila! We have successfully polluted the global object prototype.
But what exactly happened? Why did this format magically work and not the others🤔.

Let's take a look under the hood to understand what exactly is happening.

Since JSON.parse considers every key as an arbitrary string, JSON.parse(params.get("cart")) will create an object like below:

const updateObj = {
  "items": {
    "2": 3,
    "3": 1,
  },
  "__proto__": {
    "hack": "hacked",
  },
};
Enter fullscreen mode Exit fullscreen mode

Here __proto__ is just an arbitrary string and it doesn't point to the prototype object Object.prototype. We then move on to recursively merging cart object and updateObj.

At some point in the recursive merge, the function will assign target["__proto__"]["hack"] = "hacked". During this assignment the javascript runtime treats ["__proto__"] as the getter for the prototype property of Object. Hence, the assignment becomes equivalent to Object.prototype["hack"] = "hacked". Now every object created using Object() constructor function will have access to the property hack.

Injecting the hack property is pretty useless to us, so let's try to find some more useful property that would help us get that sweet 100% discount😍.

More code exploration

We need to now look for some functionality or property that can be overwritten or injected using the above method, so that we can avail the discount.

Screenshot

We see that the calculateTotal function checks if the discount object has a "truthy" property called discountCodeValid and applies the 100% discount.
Aha! If we inject the property discountCodeValid into the Object.prototype we can buy all our favourite products for free!!!

Availing the 100% Discount 🥳

https://exploit-episode-1.middlewarehq.com/?cart={"items":{"2":3,"3":1},"__proto__":{"discountCodeValid":true}}
Enter fullscreen mode Exit fullscreen mode

The above url causes the following javascript call:

cart["__proto__"]["discountCodeValid"] = true
Enter fullscreen mode Exit fullscreen mode

This inserts the property discountCodeValid into the Object.prototype object. When the function calculateTotal is called, the control goes to the if statement if (discount.discountCodeValid) {. Javascript finds the property discountCodeValid in the Object.prototype by the principle of prototype chaining and the total cost is set to 0.

Screenshot

You can now see that the total cost shown in the cart is 0 and it also says Total: $0.00 (100% discount applied)🎊.

Screenshot

Click on the buy button with the discount applied to get a special surprise😉.

Prototype pollution in the wild

In the recent years there have been numerous real-world vulnerabilities that have been caused by prototype pollution. Various javascript frameworks and libraries have been affected.

  1. jQuery (CVE-2019-11358): In 2019, a prototype pollution vulnerability was discovered in jQuery, one of the most widely used JavaScript libraries. Versions prior to 3.4.0 were affected, potentially impacting millions of websites.

  2. minimist (CVE-2020-7598): This widely-used argument-parsing library for Node.js was discovered to be vulnerable in early 2020, affecting countless Node.js applications and CLI tools.

  3. object-path (CVE-2020-15256) : Later in 2020, the object-path library, used for accessing deep properties of objects, was found to be susceptible to prototype pollution attacks.

  4. Lodash (CVE-2019-10744): In July 2019, a significant prototype pollution vulnerability was discovered in Lodash, one of the most widely-used JavaScript utility libraries. This vulnerability affected all versions prior to 4.17.12 and could potentially impact millions of projects.

How to write prototype pollution safe code

Just like we hacked the shopping site, the same can happen to our apps as well😥. We need to adopt some coding practices to prevent this from happening to our websites. Here are some key strategies we at Middleware use to write code that's resistant to prototype pollution:

  • Object.create(null): When you simply need to store objects from untrusted sources use. This creates an object with no prototype, eliminating the risk of pollution.
const safeObj = Object.create(null)
Enter fullscreen mode Exit fullscreen mode
  • Sanitising Keys: This is probably the most obvious way to prevent prototype pollution. But many a times, flawed sanitisation implementation allow attacker to still pollute the prototype using the constructor or changing the value of the key slightly to bypass sanitisation. Let's see how we can update the merge function used by the shopping site to prevent this type of attack:
function merge(target, source) {
  for (let key in source) {
    if (Object.hasOwn(source, key) && key !== '__proto__' && key !== 'constructor') {
      if (typeof source[key] === 'object' && source[key] !== null) {
        target[key] = safeMerge(target[key] || {}, source[key]);
      } else {
        target[key] = source[key];
      }
    }
  }
  return target;
}
Enter fullscreen mode Exit fullscreen mode
  • Object.freeze() : Another way of preventing change to the Object.Prototype is to use Object.Freeze. Freezing an object prevents extensions and makes existing properties non-writable and non-configurable. A frozen object can no longer be changed.
Object.freeze(Object.prototype)
obj = {}
obj.__proto__.evil = "evil"
"evil" in obj // false
Enter fullscreen mode Exit fullscreen mode
  • Map() : We can also objects like Map which provide built in protection. Although a map can still inherit malicious properties, they have a built-in get() method that only returns properties that are defined directly on the map itself.
Object.prototype.hacked = "polluted"
let safeObj = new Map()
safeObj.set("name", "John")

safeObj.hacked === "polluted" // true
safeObj.get("hacked") // undefined
safeObj.get("name") // John
Enter fullscreen mode Exit fullscreen mode
  • Dependency Security : We can take all the precautions while writing our code, but it takes only a single vulnerable library to break it all apart. So it is very important to only use secure libraries. Luckily, npm provides a built-in command called npm-audit which scans your project for known vulnerabilities.
npm audit
npm audit fix
Enter fullscreen mode Exit fullscreen mode

Final thoughts

And there you have it folks! We have successfully turned a shopping cart into our own hacking playground. But remember, with great power comes great responsibility (and potentially some very confused developers).

Meme

So, whether you're building the next Amazon or just trying to keep your JavaScript objects in line, keep this in mind. After all, you wouldn't want your users getting a 100% discount on everything, would you? (Or maybe you would, in which case, can we be friends?)

Now that you're armed with this prototype pollution prowess, why not put your skills to the test? Think you're a hotshot hacker after this little adventure? Well, we've got a challenge for you!

⚡️ Try and break the Middleware repo!

Go ahead, give it your best shot 🚀.

Bring it on

Stay safe out there in the JavaScript world, and may our objects never get polluted! (Unless, of course, you're trying to break our app – in which case, bring it on! 😛)

GitHub logo middlewarehq / middleware

✨ Open-source DORA metrics platform for engineering teams ✨

Middleware Logo

Open-source engineering management that unlocks developer potential

continuous integration Commit activity per month contributors
license Stars

Join our Open Source Community

Middleware Opensource

Introduction

Middleware is an open-source tool designed to help engineering leaders measure and analyze the effectiveness of their teams using the DORA metrics. The DORA metrics are a set of four key values that provide insights into software delivery performance and operational efficiency.

They are:

  • Deployment Frequency: The frequency of code deployments to production or an operational environment.
  • Lead Time for Changes: The time it takes for a commit to make it into production.
  • Mean Time to Restore: The time it takes to restore service after an incident or failure.
  • Change Failure Rate: The percentage of deployments that result in failures or require remediation.

Table of Contents





Top comments (13)

Collapse
 
cvam01 profile image
shivam singh

Okay this is what I call quality content🔥

Collapse
 
adnanhashmi09 profile image
Adnan Hashmi

Thank you :)
Glad you liked it

Collapse
 
jayantbh profile image
Jayant Bhawal

Crazy that libs like jquery and lodash had been affected by it!

Could functional style of programming limit the existence of this vuln?

Collapse
 
adnanhashmi09 profile image
Adnan Hashmi

Huh! A good food for thought.
Maybe a topic for another blog. 😉

Collapse
 
shivamchhuneja profile image
Shivam Chhuneja

Always 50% off sale going on?

Collapse
 
array_dot_reduce profile image
Bhaskar Ghale

Has this ever been an IRL concern outside some reported vulns?

Collapse
 
adnanhashmi09 profile image
Adnan Hashmi

Of course it is. Prototype pollution can lead to more sever attacks like XSS, RCE, bypassing authentication, request forgery and the list goes on. And prototype pollution is very easy to exploit as we saw in this blog. :)

Collapse
 
samadyarkhan profile image
Samad Yar Khan

Didn't know about this. Brilliant!

Collapse
 
adnanhashmi09 profile image
Adnan Hashmi

The more you know :)

Collapse
 
_tauqeerahmad profile image
Tauqeer Ahmad

Now, this is cool!

Collapse
 
adnanhashmi09 profile image
Adnan Hashmi

Thank you :)

Collapse
 
jhewt profile image
José A. Sachs • Edited

That's great if you want to have a rock solid frontend, free of probably scams or phishings using this, but I'd add a clarification that this will never work if there is server side checks on all items, discount codes and prices... no one in the right mind would trust a price value coming from the frontend.

Collapse
 
vipindevelops profile image
Vipin Chaudhary

What an Amazing read brother 🔥
Waiting for next one