Prototype pollution via browser APIs
You may be surprised to learn that there are a number of widespread prototype pollution gadgets in the JavaScript APIs commonly provided in browsers. In this section, we'll show you how to exploit these for DOM XSS, potentially bypassing flawed prototype pollution defenses implemented by developers.
PortSwigger research
The labs in this section are based on original PortSwigger research. For a technical insight into how we were able to discover these gadgets and more using DOM Invader, check out the related blog post.
Prototype pollution via fetch()
The Fetch
API provides a simple way for developers to trigger HTTP requests using JavaScript. The fetch()
method accepts two arguments:
-
The URL to which you want to send the request.
-
An options object that lets you to control parts of the request, such as the method, headers, body parameters, and so on.
The following is an example of how you might send a POST
request using fetch()
:
fetch('https://normal-website.com/my-account/change-email', {
method: 'POST',
body: 'user=carlos&email=carlos%40ginandjuice.shop'
})
As you can see, we've explicitly defined method
and body
properties, but there are a number of other possible properties that we've left undefined. In this case, if an attacker can find a suitable source, they could potentially pollute Object.prototype
with their own headers
property. This may then be inherited by the options object passed into fetch()
and subsequently used to generate the request.
This can lead to a number of issues. For example, the following code is potentially vulnerable to DOM XSS via prototype pollution:
fetch('/my-products.json',{method:"GET"})
.then((response) => response.json())
.then((data) => {
let username = data['x-username'];
let message = document.querySelector('.message');
if(username) {
message.innerHTML = `My products. Logged in as <b>${username}</b>`;
}
let productList = document.querySelector('ul.products');
for(let product of data) {
let product = document.createElement('li');
product.append(product.name);
productList.append(product);
}
})
.catch(console.error);
To exploit this, an attacker could pollute Object.prototype
with a headers
property containing a malicious x-username
header as follows:
?__proto__[headers][x-username]=<img/src/onerror=alert(1)>
Let's assume that server-side, this header is used to set the value of the x-username
property in the returned JSON file. In the vulnerable client-side code above, this is then assigned to the username
variable, which is later passed into the innerHTML
sink, resulting in DOM XSS.
Note
You can use this technique to control any undefined properties of the options object passed to fetch()
. This may enable you to add a malicious body to the request, for example.
Prototype pollution via Object.defineProperty()
Developers with some knowledge of prototype pollution may attempt to block potential gadgets by using the Object.defineProperty()
method. This enables you to set a non-configurable, non-writable property directly on the affected object as follows:
Object.defineProperty(vulnerableObject, 'gadgetProperty', {
configurable: false,
writable: false
})
This may initially seem like a reasonable mitigation attempt as this prevents the vulnerable object from inheriting a malicious version of the gadget property via the prototype chain. However, this approach is inherently flawed.
Just like the fetch()
method we looked at earlier, Object.defineProperty()
accepts an options object, known as a "descriptor". You can see this in the example above. Among other things, developers can use this descriptor object to set an initial value for the property that's being defined. However, if the only reason that they're defining this property is to protect against prototype pollution, they might not bother setting a value at all.
In this case, an attacker may be able to bypass this defense by polluting Object.prototype
with a malicious value
property. If this is inherited by the descriptor object passed to Object.defineProperty()
, the attacker-controlled value may be assigned to the gadget property after all.
What next?
JavaScript is now commonly used to build back-end functionality. Naturally, this means that prototype pollution can also occur in server-side contexts. In the next section, you'll learn how to safely detect and exploit these vulnerabilities in applications running on Node.js.