Protect Your Code from Sneaky Attacks: Understanding JavaScript Prototype Pollution
Have you ever wondered how you get the endsWith()
or the split()
methods in a string variable?
If you haven’t, keep reading! If you have, do keep reading nonetheless cause this article is not about that!
The following mind-map is regarding an old story of mine about how I ended up learning more about the JS prototypes while curiously checking a certain console log of npm audit
. For those who doesn’t know what is: npm audit.
Sometimes, we take the very basic & obvious conveniences for granted that we overlook the powerful magic behind it.
While doing a routine check using npm audit
, I stumbled upon the following vulnerability:
Even though, we just don’t keep fixing things with npm audit fix
, it might be a good habit to at least explore the reasons for the vulnerabilities.
Anyway, hitting my chrome search bar with the phrase Prototype Pollution cost an hour of my sleep 😴. Not that I regret it!
Long story short:
“This is a JavaScript vulnerability that can enable an attacker to add arbitrary properties to the global object prototypes. In this way, it is then possible for the user-defined objects to inherit those properties containing malicious data.”
Too many things going on that couple of sentences. Let’s break it down!
We’ll focus on-
(2)…add arbitrary properties to the global object prototypes…
&
(1)…user-defined objects to inherit those properties…
Let’s begin from the end.
(1)
How can a user defined object be able to inherit properties?
Most of the things in JavaScript is an object under the hood.
Almost everything that one can declare in JavaScript is linked to an object of some kind, known as its prototype. This sort of an inheritance happens automatically, JavaScript assigns new objects to one of its built-in prototypes. For instance, strings go under the built-in String.prototype
.
When you create a string variable or a constant, you can inspect from the browser console that it comes with a handful of properties & methods like illustrated below:
Object.getPrototypeOf(title)
shown below indicates that its prototype is String
(String.prototype):
Coming back to the opening of this article- this is how we get the endsWith()
method (marked orange above).
Furthermore, that prototype might have its own prototype, let’s dive further-
This prototype also has a prototype, and that contains a method toString()
. That’s basically the Object prototype. Despite the constant title
being a string, it still has a toString()
method which is available due to this prototype chain. It wasn’t inherited from String.prototype
, rather from Object.prototype
.
A relevant note taken from Object Prototypes :
“The prototype is itself an object, so the prototype will have its own prototype, making what’s called a prototype chain.”
The following paragraph will come in handy understanding the broken down part #2:
“When you try to access a property of an object: if the property can’t be found in the object itself, the prototype is searched for the property. If the property still can’t be found, then the prototype’s prototype is searched, and so on until either the property is found, or the end of the chain is reached, in which case undefined
is returned.”
Well, now that the basics are sorted, let’s move to the highlight of the article :)
(2)
How can an attacker manipulate (or pollute) the global object prototypes?
Prototype pollution is a JavaScript vulnerability that opens up possibilities to add arbitrary properties to the global object prototypes. Those properties can become a part of the user-defined objects due to their connection with the prototypes.
A classic example of this can be, when an object is merged recursively that contains user-controllable properties into an existing object (very detailed explanation coming up below!).
It can be as simple as taking a user input (JSON) from a text input and merging and/or constructing something from that without a proper sanitization.
Key sanitization is essential for cases like this. This can allow an attacker to inject a property with a key like __proto__
, along with arbitrary nested properties.
Why is this particular property name __proto__
important?
From Object Prototypes :
It is possible to fool your system into thinking that something legit is being pushed via that property.
Due to the special behaviour of __proto__
in a JavaScript context, the merge operation may assign the nested properties to the object's prototype instead of the target object itself.
Which leads to a wider problem.
Now, other objects may inherit this property since it’s now part of the Object.prototype
. Have a glance at the illustration below, it will help you visualize the example code snippet later.
If the illustration is not clear enough, no worries!
Below is a code snippet that merges two objects and returns the target object. Notice the construction of the two objects userObject
and the appObject
.
The merged result now has the property property1
and instead of a property with the key __proto__
, we now have it as part of the Object prototype.
There, we did it! See for yourself the log in the console:
The real deal starts now-
Let’s say we have a VIP object that deals with the payment stuff. If you inspect this object (basically any other object that has the Object prototype as its prototype), you’ll find something fishy.
Voila! That redirectUrl
property is now part of that too.
From this point onwards, this can open up a number of possibilities to make things ugly.
Although, the presence of modern JavaScript frameworks lessens the exploits that one can do using prototype pollution, it still can become a serious issue if chained with other vulnerabilities.
And that’s it folks! If you made it this far, congratulations! Now help yourself with a virtual cookie and a glass of milk (really bad editing though, just dragged & dropped). Unfortunately, virtual cookies aren’t quite as delicious as real ones. Now go forth and conquer the world with your newfound knowledge! Or, you know, take a nap like me. Thanks for reading and until next time, stay curious and keep learning!