Understanding prototype pollution
What is a prototype?
To comprehend prototype pollution, it is crucial to understand the concept of a prototype. So, let’s dive right into it. A prototype is a mechanism that allows objects to inherit features, properties, and various kinds of data from one another. Let me illustrate this with an example:
const programmingLanguages = {
coolOne: "Javascript",
godHelpMe: "APL",
};
Object.prototype.bestOne = "Haskell";
In this example, the programmingLanguages
object initially only has the properties coolOne
and godHelpMe
. However, we then utilize the Object prototype to add another property, bestOne
, which is inherited by programmingLanguages
even after its creation. This mechanism is not limited to properties alone; it also applies to functions. In the previous example, if we try to access the bestOne
property within the programmingLanguages
object, it will have inherited this property:
console.log(programmingLanguages.bestOne); // The output will be Haskell
Once you define a property in the Object prototype, all objects derived from Object will also possess that property, unless they already have their own property with the same name. It’s important to note that each type has its own prototype. For example, strings inherit from String.prototype
, and String.prototype
is derived from Object.prototype
. In fact, almost all objects inherit from Object.prototype
, as it serves as the base for all other objects and types.
Objects also have a __proto__
property, which is an internal property that points to their own prototype.
What is prototype pollution?
Prototype pollution is a vulnerability in JavaScript that allows an attacker to manipulate object prototypes, enabling them to add arbitrary properties and functions to global objects. These added properties can then be inherited by user-defined objects, resulting in unexpected behavior, privilege escalation, and, in some cases, remote code execution.
How do these vulnerabilities occur?
These vulnerabilities typically occur when a function recursively merges an object that includes user input properties with an existing object. This enables the attacker to inject a property called __proto__
along with other properties. When this __proto__
property is recursively merged, it may be assigned to the object’s prototype instead of the intended target object. As a result, an attacker can exploit this behavior by assigning harmful values to properties, which can be used in the application in a dangerous manner. Once polluted, the object and its derived objects inherit the injected properties, allowing the attacker to exploit this behavior to their advantage.
Server side impact example
Let’s see how this vulnerability can escalate privilege in a Node.js application using Express and a vulnerable version of the lodash library (4.17.4):
const user = {
username: "user",
pass: "supersecretpass",
};
app.post("/address", (req, res) => {
// Model-like object
const address = {
street: "John Doe",
number: 1337,
city: "Neverland",
};
if (!req.body.street && !req.body.number && !req.body.city) {
return res.status(400).send("Invalid Payload");
}
lodash.merge(address, req.body);
return res.status(200).send(`Address created: ${JSON.stringify(address)}`);
});
app.get("/admin", (req, res) => {
if (!user.isAdmin) {
return res.status(403).send("You're not an admin");
}
return res.status(200).send("FLAG{Oh_s0_y0u'r3_th3_h4ck3r}");
});
In the code example above, we can only access the /admin
endpoint if the isAdmin
property of the user object is true. As we can see, the user object does not have this property, so it is false, and the server responds with You're not an admin
. But what’s the relation of this with prototype pollution?
In fact, we can access the /admin
endpoint by polluting the payload received in the /address
endpoint. If we send the following payload:
{
"street": "John Doe",
"number": 1337,
"city": "Neverland",
"__proto__": {
"isAdmin": true
}
}
This payload will pollute the user object due to the line lodash.merge(address, req.body);
. When the function runs, it recursively merges the payload’s body, even hitting the Object.prototype
, and it will be inherited by all derived objects. Therefore, the user object will now have the isAdmin property set to true, and if you try to request the /admin
endpoint, the flag will be returned.
So, this is an example of privilege escalation using prototype pollution. However, you can also cause unavailability of the target by overwriting the toString
function, for example:
{
"street": "John Doe",
"number": 1337,
"city": "Neverland",
"__proto__": {
"toString": "I guess you're going to stop working"
}
}
Preventing prototype pollution
One of the easiest ways to prevent prototype pollution is by sanitizing the keys before merging them with other objects. This helps prevent attackers from injecting any references to the object prototype. It’s important to note that sanitizing only the __proto__
key will not be sufficient. There are other ways to accomplish this, such as using constructors or employing obfuscation techniques if your validation is weak, among others. However, a better approach to prevention is to freeze the Object.prototype
like this:
Object.freeze(Object.prototype);
This ensures that the properties of Object.prototype
cannot be modified or added to the prototype.
Conclusion
Prototype pollution is a critical vulnerability that can have severe consequences for JavaScript applications. Understanding the concept and taking preventive measures is crucial to ensure the security and integrity of your code. By sanitizing input and freezing the Object.prototype, you can significantly reduce the risk of prototype pollution attacks.
Remember to stay vigilant, follow best practices, and stay informed about emerging security vulnerabilities to protect your applications effectively.