Prototype Pollution Primer for Pentesters and Programmers
By Ben Berkowitz and Phil Sofia on 8 September, 2022
Introduction
Prototype pollution is a vulnerability that exploits inheritance behavior in JavaScript to create malicious instances of data types, which in the right conditions, can result in the execution of attacker-supplied code. Depending on the context, this can have impacts ranging from DOM-based Cross Site Scripting to even Remote Code Execution. What sets this apart from other attacks, such as “standard” Cross-Site Scripting, is that the impact targets the prototype that other data types will inherit from.
Before embarking on this research, we had heard of prototype pollution but had a difficult time wrapping our heads around it. We’d seen the term used often in the context of vulnerable JavaScript libraries, but didn’t have a solid understanding of what the impacts could be or how they came about. So, we turned to the existing published works on the subject.
In 2018-2021, there were several articles and papers published regarding prototype pollution that showed significant much of an impact it could have in specific situations:
- In 2019, Michał Bentkowski discovered a method of exploiting a prototype pollution in the Kibana data visualization utility that made it possible to cause an application server to execute system commands and gain remote command execution.
- Olivier Arteau’s 2018 publication identified a litany of NodeJS libraries vulnerable to prototype pollution, including remote code execution in the Ghost CMS content management system.
- In 2021 (after we had already been inspired by their work), a team of researchers including Sergey Bobrov and s1r1us made a publication that landed prototype pollution on Portswigger's Top 10 web hacking techniques of the year.
While these publications are impressive, they were a lot to process for our initial exposure to prototype pollution. To benefit our own understanding of the vulnerability, we thought it was best to start with a simpler proof of concept, such as an alert window, before moving on to CVE-worthy discoveries. So, we decided to get hands on with the material in a sandbox setting.
Thanks to the work done by researchers on a repository maintained by Sergey Bobrov, we created a test page and began reverse engineering prototype pollution attacks using the browser’s debugger until we had figured out exactly what was going on. In this piece, we’ll explain to you what we learned and demonstrate how to exploit the most recent version (v 3.6.0) of jQuery’s ajax() function in the process.
We’ll cover what JavaScript prototypes are, and how they can get polluted. Then, we’ll break down the structure of prototype pollution attacks into two stages. You’ll be able to follow along with each stage step by step to see exactly how prototypes get polluted, and how even functions from well-maintained and mainstream libraries can handle them insecurely. Finally, we’ll cover some remedial actions to mitigate prototype pollution.
This article has a companion HTML demo page where you can experiment with payloads and follow along with the sections in real time. It’s best to pull up another tab with this test page while reading to get the full context and prototype pollution experience. We built this page to demonstrate prototype pollution behavior in real third-party libraries.
Visit this article’s demo page at: https://withsecurelabs.github.io/prototype-pollution/protoPollution/prototypePollution-demo.html
Follow along there as we go!
What is Prototype Pollution?
The Mailroom Analogy
Before we get too far into the jargon of JavaScript objects and inheritance, it can help to frame the concepts through an analogy.
Imagine we have a mailroom. Flowing through it are hundreds of things like letters, envelopes, books, paper airplanes, and more - all made of paper. In this imaginary, oversimplified mailroom, all of these items are made with the exact same variety of paper sheet: a white, 8.5 x 11 inch, 20 pound piece of printer paper. We'll call this original sheet the "8.5 x 11," and it is the "prototype" that all of the items coming through our mailroom are made from.
Using the 8.5 x 11 as a base, we can create other items that aren't strictly 8.5 x 11's, but inherit many properties from our original sheet. By writing or printing a message, we can create a letter, for example. Or, by including a recipient, source and monetary amount on an 8.5 x 11, we can write a (comically large) check. By folding it just right, we can create a paper airplane or origami crane. Checks, letters and paper airplanes are clearly distinct from each other, but they all inherit many of their properties from the original 8.5 x 11. If we were to change the color of the 8.5 x 11 to green, any new letters, checks, or cranes we created then will be green too. If we increased the paper's weight, we'd still have our paper creations, but they would be heavier.
Imagine, however, if instead of changing color or weight, which are properties that probably don't have much of an effect on the items we make from our 8.5 x 11's, someone came along and did something more specific and disruptive. One example would be writing a big message in all caps across the middle of every single sheet.
If this ruffian wrote "RETURN TO SENDER" on every page, then problems would start with some of the items the paper was turned into. Sure, our paper airplanes would still fly, but the postal service would never deliver any envelopes we created. If the word "VOID" was written on every single page in big letters, the origami cranes wouldn't mind very much, but no bank would treat any check we wrote as valid.
Even worse, if this miscreant (named Mal Icious) could print a line saying "Pay to the order of: Mal Icious" on every page, then they could increase their bank balance every time the mailroom staff tried to write a new check.
Malicious modification of our "prototype" (the 8.5 x 11), can cause impacts later on in very specific cases. When something that inherits properties (color, weight, the word "VOID") from the prototype inherits something nasty that affects its functionality (all checks becoming invalid), the prototype has been "polluted" in such a way that causes an impact.
While the transition from the mailroom analogy to real world JavaScript isn't completely seamless, we hope you'll see how these same concepts apply.
No More Paper
JavaScript is object-oriented: it utilizes objects that have properties and functions that can be inherited from other objects, and all objects inherit from the prototype Object.prototype.
The Object Prototype
The properties of Object.prototype will be, by default, inherited by other objects created in the DOM. If you add a property to the prototype, for example, "color", all objects created before and afterward will possess the property "color" and its corresponding value.
By default, all objects inherit properties from the prototype
Using the __proto__ operator, often called "dunder proto" (for double underscore proto), it is possible to access Object.prototype as an attribute of another object. For example, anObj.height points to the height property of anObj, but anObj.__proto__.height points to the height property of Object.prototype. Any changes we make to anObj.__proto__ will be made to the prototype (or anObj[__proto__] ), and will be inherited by other objects created either before or after we executed the changes.
Editing the prototype with __proto__
Nearly every single data type in JavaScript inherits from another type, and the common ancestor of all of these types is Object. This includes data types like Number, String and even Boolean. This means that instances of other data types that inherit from Object will possess any of the properties added to Object.prototype.
Many datatypes inherit from Object, and its prototype
Prototypes themselves are objects, and their prototype is Object.prototype. By using multiple __proto__ operators, it is possible to access the Object prototype from a different datatype entirely due to inheritance.
Prototypes are objects, as seen by chaining __proto__ operators
Modifications can also be made to a prototype's methods, not just properties. After this change, the overwritten method will be called instead of the original for all instances of that data type. For example, you can overwrite the array datatype's sort() method to do something completely besides sorting the array's contents. Here, we change it so it logs a string to the browser console.
Overwriting methods at the prototype
It isn't common for applications to edit prototypes directly, but occasionally a bug will find its way into the code that allows them to be changed in unexpected ways. If an attacker can make changes to prototypes within an application by exploiting a client-side vulnerability, this is known as prototype pollution.
If we continue to follow our mailroom analogy, Object is our 8.5 by 11 that all other mailroom items inherit from. When someone came into our mailroom and made changes to the 8.5 by 11 that impacted all of the items we made that paper into, they were performing real-life prototype pollution.
The Two Stages
While the console examples in the previous section illustrate the concepts behind prototype pollution, they do not resemble a realistic exploitation of a prototype pollution vulnerability. In reality, an application needs to be vulnerable in two ways to produce a security impact from prototype pollution.
First, the application needs to have a client-side vulnerability that allows the prototype to be polluted without manually doing so in the console. Next, it needs to call a function that processes a polluted object in a way that can be exploited to produce impact. During our research, we referred to these two stages as pollution and exploitation, respectively.
Imagine an application that builds itself programmatically by fetching external content like scripts and adding them to the DOM one by one. The application might have a fetchScript() function that accepts objects, extracts a source URL from them, and creates a script tag on the page. If somehow the objects containing the source URLs were to be polluted by an attacker, the application may then add a script to the page from a malicious source.
In this imaginary application, the exploitation stage would occur when fetchScript() retrieved a malicious URL from a polluted object it was passed. The pollution stage would have to occur before this object was passed so the object's properties would contain the malicious values.
The snippet below shows an oversimplified example of this in action. It also does not resemble a real-world example very closely, but all of the concepts are present. You can view this oversimplified page in action at https://withsecurelabs.github.io/prototype-pollution/protoPollution/overSimple.html
(The browser may produce an alert window when the overly simple page loads.)
An oversimplified page vulnerable to prototype pollution
Modification of the Object prototype, and the pollution stage, is hard-coded into this oversimplified page with Object.prototype.src = "data:,alert(123)";. Because of this line, any objects created will have the src property with the value of "data:,alert(123)". Exploitation occurs when fetchScript() transfers the new script's source from obj.src. Since obj.src was never set to be different from the "polluted" value, it defaults to "data:,alert(123)" and fetchScript() adds <script src="data:,alert(123)"></script> directly to the body of the document. This tag causes any JavaScript at the source location to execute on the page, in this case producing an alert window. The HTML of this page after the script tag as been appended is shown below:
While you're not likely to find an application that hard-codes payloads like that directly into its own prototypes, functions like fetchScript() are less rare. Commonly used JavaScript libraries can contain functions that are vulnerable to the exploitation stage, due to the fact that they dynamically write HTML based on properties from an object.
When pollution does occur, it can be due to the other side of this equation: where an application deserializes data into an object. If this data contains operators such as __proto__, as in the example outlined in the next sections, the code may write it to the prototype instead of the intended individual object.
The Demo Page
To perform the bulk of our research, we created a test page that uses two real-world libraries, one for each stage. With this page, we were able to isolate and analyze prototype pollution behavior in a way that simulated what we had seen in the wild instead of only playing around in the browser console.
For the exploitation stage, we call on functions in the most recent version of jQuery (3.6.0) to produce DOM-based Cross-Site Scripting, and for pollution, we exploit a vulnerability in an old (but still commonly utilized) third-party jQuery extension library called jQuery BBQ (version v1.3pre; SHA256 message digest: 1a232ef4c22d12b98cf89dffcc2c7f2d4cfac7f5de8adc5bf5abbfe1adf47532).
Find the demo page at https://withsecurelabs.github.io/prototype-pollution/protoPollution/prototypePollution-demo.html
Opening the demo page's HTML file in your browser will allow you to experiment with three different prototype pollution types affecting jQuery 3.6.0. We invite you to follow along in your browser's debugger as we go through both stages step by step in the next sections.
The demo page uses real releases of jQuery and jQuery BBQ that we did not modify.
Stage One: Pollution in jQuery BBQ
BBQ's deparam() As A Pollution Vector: Overview and Payload
jQuery BBQ (https://github.com/cowboy/jquery-bbq) is a third-party jQuery extension library which provides Back Button and Query (BBQ) features. While this library was last updated in 2010, its features remain useful and is utilized in applications all the way to the present day (2022).
One of BBQ's functions is deparam(), which aims to do the opposite of jQuery's param() function. It deserializes a string of name/value parameters into a new object.
BBQ's deparam() function
However, if the string fed to this function contains crafted values such as the __proto__ operator, deparam() may add new properties to Object.prototype instead of to the expected new object. The unintentional addition of properties to Object.prototype instead of the new object is the definition of protoype pollution.
If we feed the demo payload __proto__[propName]=propValue to deparam(), the function first tries to break it down into parts before assigning those parts as properties of a newly created object by iterating over an array of name/value pairs it parses out of the string.
However, deparam() parses __proto__ and the propName sections as different keys, causing these values to be assigned to Object.prototype (via deparamsReturningObject[__proto__]), instead of the new object it created to return. After the loop completes, it will have added a new property called propName to the Object prototype.
New property propName added to the Object prototype
Since all other objects inherit from Object.prototype, all objects in the DOM will inherit the propName property and its value of propValue. Merely executing deparam() on this malicious string is enough to perform prototype pollution; the object returned by the function is immaterial.
In addition to jQuery BBQ, many publicly published libraries contain prototype pollution vulnerabilities and researchers submitted reports that did not rely on BBQ to produce pollution. In theory, an attacker could use an existing Cross-Site Scripting vulnerability to inject JavaScript that polluted the Object prototype instead of directly executing a different payload. However, in most circumstances this two-staged attack would be redundant.
Pollution alone is not enough to cause a security impact. An application must implement a polluted object in an exploitable way in order to add the payload to the DOM.
Step-by-Step deparam() Pollution on the Demo Page
We found this payload, as well as the payload we use in the Exploitation section, in Sergey Bobrov's client-side-prototype-pollution GitHub repository. Huge thanks to Sergey and the other contributors for their efforts. The repository gave us enough information to craft the demo page and reverse engineer jQuery and BBQ to figure out prototype pollution ourselves.
Commonly, prototype pollution attacks are triggered by a payload carried within a URL that produces DOM-based reflected XSS. This is what we observed in the real world, so we created the demo page to recreate this.
This section, and its payload, aims to demonstrate the pollution stage only; it does not result in exploitation (DOM-based XSS). Pollution alone does not directly result in a security impact. To be vulnerable, an application must also interact with the newly created property on Object.prototype in a way that can be exploited. In the Exploitation section, we will give a step-by-step walkthrough of how that occurs using a different payload.
To follow along with this section, visit the demo page in your browser and bring up the debugger. We used Chrome (v. 100.0.4896.127) and did not "prettify" any of the files. If you don't want to follow along in detail, you can follow these quick steps to view the pollution in your own browser before moving to the Exploitation section.
Pure Pollution Quick Steps
- Visit the demo page at the URL: https://withsecurelabs.github.io/prototype-pollution/protoPollution/prototypePollution-demo.html#__proto__[propName]=propValue
- Bring up the console and enter the following:
- myObject = {};
- console.log(myObject.propName);
- Observe that your newly created object contains the polluted value from the URL
Pure Pollution Detailed Steps
Your line numbers may be slightly different than the ones we had. But look to the images attached to see where to set your breakpoints.
0. Visit the demo page in your browser at https://withsecurelabs.github.io/prototype-pollution/protoPollution/prototypePollution-demo.html#__proto__[propName]=propValue
1. Set a breakpoint at the line on the demo page that states:
var urlHash = $.deparam(location.hash.slice(1));
Then refresh so execution stops at that point. For us, this line was at line 157 on the demo page.
To unset a breakpoint in Chrome, right-click it and select Remove Breakpoint. This will be good to know as we continue.
Here the page invokes BBQ's deparam() and passes it the string generated by location.hash.slice(1). The object generated on this line, which we called urlHash, is not used by the page after creation. Pollution occurs during the execution of deparam() on this string - we can disregard what the function returns.
2. Step once (F9 in Chrome) to arrive at line (467 for us) within jquery.ba-bbq.js:
Here, deparam() creates an object which it will eventually return. Line 475 creates cur, which points to the newly created object.
3. Set breakpoints at 481, 493, and 511.
Then, continue (F8) to the breakpoint at 481. (keys_last = keys.length - 1;)
At this point, the function is trying to parse the query string into individual key-value pairs. After line 480 executes, deparam() has created an array called keys that contains only one value, __proto__[propName].
4. Continue (F8) to 493, and see that the function has divided this value in two, and now __proto__ and propName occupy different indices.
5. Continue (F8) again to 511, where the function attempts to parse the values assigned to the keys passed in the string.
After line 504 executes, the string val is equal to propValue.
6. Place a breakpoint at lines 521 and 523, then continue (F8).
Line 521 begins the for loop that is the core of deparam(). This is where the function iterates over the keys array and attempts to assign properties and values to the newly created object pointed to with cur. Continue (F8) to 523.
This loop will iterate until the counter i is greater than the value keys_last generated earlier. The code explicitly set i to start at 0, and keys_last to 1, so the loop will run twice. That's once for each index in the keys array: __proto__ and propName.
On the first pass, the function has pulled out the string __proto__ and has set it to the value of key. cur points to the object deparam() created when it began executing. BBQ's author wrote line 523 to be very compact, it's tricky to understand at first glance. To better analyze it, we've rewritten it as follows:
At this point, i equals zero, keys_last equals 1, cur points to our newly created object, and key contains __proto__. Since i < keys_last, line 523 executes as:
cur = cur[key] = cur[key]
When we plug in our values, we get the following operation represented by pseudocode:
(pointer) = theNewObject[__proto__] = theNewObject[__proto__]
Most importantly, theNewObject[__proto__] refers to Object.prototype. Meaning that after we execute line 523, cur will no longer point to the new object, but the object prototype itself!
7. Continue (F8) to enter the loop's second iteration, where i = 1. This is where the pollution actually occurs.
Now i and keys_last both equal 1, cur points to Object.prototype, and key contains propName. val, which was not relevant on the first loop, remains equal to the string propValue. As i is no longer less than keys_last, line 523 executes as:
cur = cur[key] = val;
When we plug in our values this time, our operation represented by pseudocode is:
(pointer) = Object.prototype[propName] = "propValue"
This assigns Object.prototype the propName property and gives it a value of "propValue". By editing Object.prototype directly, we affect all other objects in the DOM. On creation, every object will have the propName : "propValue" property by default. The prototype has been polluted.
Stage Two: Exploiting jQuery's Vulnerable ajax() Function
jQuery's ajax() as An Exploitation Vector: Overview and Payload
Applications can be affected by prototype pollution in ways that have minimal security impact. It does not matter much if an attacker can pollute objects if those objects are never processed in a way that, for example, adds malicious HTML to the DOM.
Separately, there may be functions that cannot be used to cause prototype pollution, but may produce impact if they handle a polluted object. During our research, we came across functions like this in the ubiquitous jQuery.
The jQuery JavaScript library is a cornerstone of modern web applications due to its open source licensing, powerful features, and active maintenance community. Like all software, however, it contains bugs and vulnerabilities that can impact security. During our research, we did not identify any jQuery (v 3.6.0) functions that could be (reasonably) abused to cause pollution, but there were functions that processed polluted objects in a way that could be exploited to execute user-supplied JavaScript.
Once again, we want to give a big thanks to the client-side-prototype-pollution repository. It gave us the following payload and provided an example using jQuery's getScript() function, but we reverse engineered how the payload worked and adapted the example for ajax() ourselves.
Using the payload below on the demo page will cause it to act very similarly to the unreasonably simplified example from The Two Stages section, but with a more realistic implementation. Instead of directly polluting the Object prototype on the page, pollution occurs due to the use of deparam() as described in the previous section. The simple example created a script tag with a src value copied directly from an object, but here, the polluted prototype exploits ajax() into adding a script tag to the DOM. This tag's src attribute will match the URL provided in the payload, producing a tag like this:
<script url="data:,alert(123)//" datatype="script" src="data:,alert(123)//"></script>
In this payload, we used the data URL data:,alert(123)// for demonstrative purposes, but an attacker could instead use a URL that points to their more impactful malicious script file, such as https://EvilAttackerSite[.]XSS//. data: is a URL scheme, like http:, that instructs browsers what kind of protocol they should use to retrieve a resource. In the case of data:, the browser doesn't actually request anything, but renders content that is contained within the URL itself.
After deparam() pollutes the object prototype with this payload, which will happen just like it did in the previous section, all objects will contain the following properties by default:
Obj.url = "data:,alert(123)//"
Obj.dataType = "script"
When ajax() processes the object passed to it by the demo page, the polluted prototype causes it to replace the target URL of localhost with data:,alert(123)//. Furthermore, the polluted dataType property causes ajax() to recognize that it will be expecting a response containing a script, which causes it to append a script tag to the page with a src attribute defined by the object.
If you don't want to follow along in detail, simply visiting this URL will cause your browser to display an alert window:
Step-by-Step ajax() Exploitation on the Demo Page
Like in the previous Step-by-Step section, we encourage you to follow along with your browser's debugger on the test page after visiting the following URL with the payload. Again, we used Chrome and did not "prettify" any of the files.
Be very careful with your stepping and breakpoints here, as these functions use nested loops and plenty of recursion. Make sure you have unset all of your breakpoints within BBQ before starting.
Your line numbers may be slightly different than the ones we had. But look to the images attached to see where to set your breakpoints.
0. Visit the demo page in your browser at https://withsecurelabs.github.io/prototype-pollution/protoPollution/prototypePollution-demo.html#__proto__[url][]=data:,alert(123)//&__proto__[dataType]=script
(This will produce an alert box containing "123" in your browser).
Before the demo page executes the next step, it calls deparam() on the string from the URL hash, and pollutes the prototype. These steps illustrate exactly how this pollution results in DOM XSS, but keep in mind that ajax() has nothing to do with the pollution stage described in this article.
1. Set a breakpoint at line 173 on the demo page.
Here the demo page invokes ajax() by passing it a simple object with just two explicitly defined properties, url and cache:
In testing, we found that the payload triggered DOM XSS as long as this object contained url and at least one other property, but the value of this second property didn't matter. We chose {url: "localhost", cache : false} because it would be a better approximation of a realistic scenario than a meaningless dummy property, e.g {url: "localhost", aaa : "aaa"}.
2. Next, step once (F9). The browser enters into jQuery's ajax() definition on line 9381, where it checks if what has been passed is a string containing a URL or an object that might contain a URL in a property.
It calls this argument url, and if url is an object, it is explicitly undefined. Later, the fact that the variable called url at this level is undefined will be critical in replacing the target URL with the payload destination.
3. Place a breakpoint at line 9417 and continue to it (F8).
The initial object that was passed from the demo page does not contain enough information to form a full request, so ajax() calls a function ajaxSetup() to create the "final" options object that contains information such as request headers, content type, etc. This "final" object is simply called s, and will be used to make the request.
As arguments, ajaxSetup() takes a new object, which will be used to form the request target, and the options object that contains the values passed from the demo page.
While you're in the neighborhood, set a breakpoint on line 9522 before advancing.
4. Step once (F9) and arrive at line 9365, the definition of ajaxSetup(). Since ajaxSetup() has been passed two parameters, and the second one, settings (which we passed from the demo page), contains a truthy value, the ternary operator ? will proceed to execute the statement on line 9368.
This line calls another function, ajaxExtend(), twice. The first time around, it creates an object based on default settings. On the second pass, it uses this object as the target and uses the settings passed from the demo page. Due to prototype pollution, the url property generated in objects on both of these passes will have a value containing our payload, data:,alert(123)//.
5. ajaxExtend(), which is already called twice, builds on another function called jQuery.extend() which calls itself up to four layers deep.
To avoid getting pulled into a recursion whirlpool, we'll be very careful with our breakpoints and stepping here.
Step once (F9), then step out (Shift+F11) to arrive at the second pass of ajaxExtend() on line 9368.
You'll see that the target object's url property has already been polluted by the first pass, but we'll look into the second pass more closely.
6. Step once again (F9), and then set a breakpoint at line 9120 and continue to it (F8).
Before we execute jQuery.Extend(), we can see that target.url equals localhost and target.dataType = "script"
Step (F9) and enter into jQuery.extend() on line 259. This is where the recursion and looping occurs, so continue to be extra careful while following along.
Place a breakpoint on line 290 and continue (F8) to it.
This is the for loop that will iterate over the polluted object to transfer the malicious URL value to the target.
7. Before we begin iterating, see that line 291 created yet another object called options based on the arguments passed to jQuery.extend()
options has only one explicitly defined property: the cache property that was passed along all the way from the demo page. However, since we are iterating over this object with a for loop, we will access the properties added by pollution, options.url and options.dataType.
Step twice (F9 x 2) to see that the code extracts options[name], which at the moment is options["cache"] and stores it in a variable called copy.
On line 295, jQuery makes an effort to prevent prototype pollution by checking to see if the name of the property it is looking at is __proto__. While this check might prevent us from polluting the prototype at this step with a property named __proto__, our pollution occurred before we even called ajax() on the demo page and this line can't undo that.
Set a breakpoint at line 319 and continue (F8) to it.
This line transfers the value of copy to target[name], which at the moment evaluates as target["cache"]= "false"
8. Step three times (F9 x 3) and see that even though the options object only has cache : "false" explicitly defined, the for loop has chosen url as the next property to transfer to the target object.
Since jQuery.extend() calls itself, we can't just continue to our breakpoint at line 315 without dropping down another level of recursion. Instead, we'll unset the breakpoint at 319, put a breakpoint at 315 and continue to it (F8).
Here, we can see that target[url] contains the value passed to it from the demo page, but copy contains the URL we set via pollution. We also know that just like we did for the cache property, we will transfer copy's value into target. However, since copy is an array this time around (with cache it was a string), jQuery will execute line 315 to set target[url].
Navigating the execution of this line with breakpoints is a complicated mess of recursion, but the important takeaway is that at this stage, line 315 executes as:
target[url] = ["data:,alert(123)//", dataType: "script", url: Array(1)]
This sets target[url] to the polluted payload.
9. Unset the breakpoints at 290 and 315, which should leave you with no breakpoints inside jQuery.extend().
Step out (Shift + F11) to return to line 9123 within ajaxExtend(), then unset the breakpoint at line 9120. Here we see that the target object it has generated and is about to return to ajaxSetup() contains the polluted URL (data:,alert(123)//), not the localhost URL provided on the demo page.
10. Step Out (Shift+F11) to return to ajaxSetup(), and then continue (F8) to arrive at the breakpoint we set earlier at line 9522 within the main ajax() function.
By the time we've arrived at this line, we've finished creating s, the "final" options object that ajax() will use to make its request. On line 9522, ajax() uses an or statement to set the value of s.url.
In JavaScript, definition using or statements like this assigns the first value that is not undefined when read left to right. Way back in step 2, we saw that ajax() explicitly undefined the string called url, and it remained that way. Therefore, this line keeps the value of s.url unchanged (and equal to the polluted value), as s.url is the first defined value when read left to right.
As a result of this line, ajax() is only vulnerable to this payload because the demo page passes it an object, not a string. ajax() wrapper functions, such as jQuery's getScript(), getJSON(), get(), and post(), are vulnerable to this payload because they craft their own objects to pass to ajax().
12. Before 9522 executes, step three times (F9 x 3) to arrive at line 9532. The execution of 9522 and 9522 took the s.url and s.dataType values from our polluted prototype and cemented them within the final s object:
13. Place a breakpoint at line 10173 within jQuery.ajaxTransport() and continue (F8) to it. At this point, jQuery is "aware" that its request has the "script" datatype, and as a result will craft a script tag to append to the page based on the s object.
14. Set a breakpoint at line 10185, and continue (F8) to it. This is the line that delivers the payload to the DOM and causes our JavaScript to execute.
document.head.appendChild(script[0]) appends the script tag, with a src attribute of our payload, to the DOM.
15. Continue (F8), and the payload will execute.
Mitigating Prototype Pollution
Prototype Freezing
For optimal defense in depth, all components used by an application would contain mitigations that both prevent pollution and negate the effects of its exploitation. However, as a single pollution vector can result in the exploitation of multiple functions, it is most efficient to focus on preventing pollution from occurring in the first place.
The most effective measure to prevent prototype pollution is to prevent an application from altering its prototypes entirely. If an application does not require the ability to alter the Object prototype, the application can freeze it to prevent pollution in most cases. Object.freeze(Object.prototype) will prevent adding, removing or changing the value of the prototype's properties but does not prevent the explicit definition of properties on individual objects. While the application will no longer be able to make changes to the Object prototype, individual objects will behave normally.
Prototype freezing is irreversible, and will be applied across the entire DOM. Therefore, we suggest placing Object.freeze(Object.prototype) where it will be executed before any other functionality can pollute the object prototype. This will allow the other components, which may be maintained by a third-party, to be altered as needed without calling into question the application's mitigations against prototype pollution.
We recommend prototype freezing whenever possible, as individual functions may contain components that are only partially effective against pollution attacks. For example, the most recent version of jQuery at the time of this writing (3.6.0) contains effective protections against its functionalities being used to pollute prototypes. However, these protections do not reverse pollution that has already occurred when those functions are called, allowed them to still be used to produce impact.
Avoiding Components with Known Prototype Pollution Vulnerabilities
Many publicly available libraries contain known prototype pollution vulnerabilities that result in both pollution and exploitation. Security researchers maintain lists of these libraries, along with the payloads used to both pollute and exploit them. During our research into this topic, we frequently referenced the work of researchers who maintain such a list at the client-side-prototype-pollution (https://github.com/BlackFan/client-side-prototype-pollution) GitHub repository.
Vulnerable open-source libraries may stop receiving updates and patches addressing prototype pollution may not be published. The jQuery BBQ project stopped receiving updates in 2010, and as a result may contain other vulnerable functions in addition to prototype pollution. We recommend replacing any functionality provided by this library with a more current alternative. In the short term, applications that rely on BBQ should implement prototype freezing when possible until a suitable alternative is found.
Input Validation
As a defense-in-depth measure, and when it is required that an application make frequent, dynamic modifications to the object prototype, the application can implement filtering that targets prototype pollution payloads. These filters should reject or sanitizes user-supplied input containing strings such as __proto__ or [constructor][prototype], especially when creating object properties or assigning object property values. When possible, this filtering should follow an allow-list approach, as attackers often employ methods such as character encoding to bypass block-lists.
Additional Reading and References
Sergey Bobrov's Client-Side Prototype Pollution Repository
https://github.com/BlackFan/client-side-prototype-pollution
Prototype Pollution: The dangerous and underrated vulnerability impacting JavaScript applications
https://portswigger.net/daily-swig/prototype-pollution-the-dangerous-and-underrated-vulnerability-impacting-javascript-applications
After three years of silence, a new jQuery prototype pollution vulnerability emerges once again
https://snyk.io/blog/after-three-years-of-silence-a-new-jquery-prototype-pollution-vulnerability-emerges-once-again/
Prototype Pollution attack in NodeJS Application
https://github.com/HoLyVieR/prototype-pollution-nsec18/blob/master/paper/JavaScript_prototype_pollution_attack_in_NodeJS.pdf
jQuery BBQ Core Library File
https://raw.githubusercontent.com/cowboy/jquery-bbq/master/jquery.ba-bbq.js
jQuery BBQ GitHub Issues on Prototype Pollution in deparam()
https://github.com/cowboy/jquery-bbq/pull/61
https://github.com/cowboy/jquery-bbq/issues/62
"A tale of making internet pollution free" - Exploiting Client-Side Prototype Pollution in the wild
https://blog.s1r1us.ninja/research/PP
Thanks for reading!