Prototype Pollution
prototype pollution, injection attack, objects in JavaScript, prototypes
Hey there, hope you are doing great. In this article, we will dive deep into what prototype pollution is and I will be sharing the things that I learned while exploring this vulnerability, As mentioned in my previous article this is a kind of Injection Attack that allows the attacker to control the default values of the objects and make your application do some unexpected and weird things.
Link to my previous post - JavaScript Prototype
Exploring The Project
To better understand this I am taking an example application and after thinking a lot I came up with this cool name for this application and its called "Employee Management System ", this application is responsible to manage the employee data. The database contains the employee details along with the access details of the employee.
Mentioned below are some of the information about the API.
Endpoints:
// creates a new Employee
POST: http://localhost:3000/employes
// List all the Employee
GET: http://localhost:3000/employes
// Return Employee by ID
GET: http://localhost:3000/employes/{eid}
// Get Employee by Role
GET: http://localhost:3000/employes/role/{role}
// updates Employee basic details
PUT: http://localhost:3000/employes/{eid}
// Only Admin can updates employees access details
PUT: http://localhost:3000/employes/admin/{aid}/employee/{eid}
// Only Admin can delete employees details
DELETE: http://localhost:3000/employes/admin/{aid}/employee/{eid}
Below is the request that can be used to submit or update employee's basic details.
{
"employeeDetail": {
"fullName": "string",
"department": "string"
},
"primaryContactDetails": {
"workEmail": "string",
"workPhone": "string"
},
"additionalContactDetails": {
"personalEmail": "string",
"personalPhone": "string"
}
}
There are two roles("emp", "admin") defined in the system for employee and admin respectively.
Below is the request that an admin user can use to update employee's access details.
{
"accessDetails": {
"role": "string",
"sys_adm_access": boolean,
"wrk_adm_prtl": boolean
}
}
Playing with the API
Let's check the Get All Employee's Endpoints
It returns empty response means there is no employee data available in the database.
Get Employee By Role Endpoint
As expected nothing in this call too.
When I was asked to work on this project, I was told that there is one admin user created to verify the admin related functionality, so lets check if we can find that admin user with get employee by role endpoint.
Looking at the response, admin user's details are concealed except for the name, ensuring that sensitive information remains hidden from regular users. Additionally, the "get all employee" feature does not disclose admin details as we saw above, further safeguarding their privacy.
As a result, accessing admins information is only feasible through direct database access. This restriction extends to the "get employee by ID" functionality as we cannot get the id of admin user other than directly looking into database. Overall, the system effectively preserves admin confidentiality.
Let's try to add some employee
Attempt-1: (submitting employee details with access details) - We know that the endpoint exposed to submit employee details does not allow employees to submit their access details, so let's try to verify that by passing the access details object while creating the employee.
Alright, it's blocking us to pass the access details and prompting us to reach out to the admin, returning a 403 Forbidden or Unauthorized status code.
Nothing added in DB as well, except for the already existing admin user.
Attempt-2:(submitting employee without access details)
Success! The employee has been successfully created.
And it's also recorded in the database! What's the next check now?
How about the update functionality? Let's give that a look as well.
Attempt-1: (Updating access details of employee without admin): According to the API documentation, only an admin user can update an employee's access details, but let's confirm that.
The response indicates a 403 error, preventing an employee from updating their access details. Based on these findings, it appears that the API effectively prevents regular users from modifying access details.
Now it's time for the long-awaited Prototype Test.
Are the Endpoints Immune to Prototype Pollution!!
Attempt-1: (Verifying the update endpoint for prototype vulnerability)
Let's check the update endpoint first, and see if we can update the access details.
Here I am using __proto__
to modify the prototype property of the request and see if the application falls for this trick.
Seems like the contact details and name is updated, let's check the access details.
No change in the access details, so it worked as expected.
What about the create employee endpoint, is it also immune to prototype pollution?
Attemp-2: (Submitting a new Employee and verifying if the access details can be modified while creation )
Following the same approach and using __proto__
to modify the prototype property of the request and see if the application falls for it this time.
Interesting, The employee has been created. Now, let's verify if**John is added to the admin list.
There you go, John is now the admin !!!
This implies that if I were an attacker, I can now manipulate the system to create an admin user. Now, let's confirm if this admin user can indeed perform administrative tasks. Let's attempt to update the access details of EMP1903.
Here's how the access details of EMP1903 looks like at the moment.
Hitting the update endpoint with admin John to update the access details of EMP1903.
Success!! Access details has been updated for EMP1903. Let's verify it by calling get employee by id.
Indeed its updated and its reflecting in the Database as well !!
This show's that the application was successfully manipulated to perform a operation that it was not suppose to perform in an ideal situation, this test successfully demonstrated how the application was tricked to first create a admin user and then use that admin user to update the access details of other user.
This highlights how the __proto__
property can be exploited or polluted, rendering your application vulnerable to prototype pollution.
What really went wrong with the application?
For the most part the API behaved as expected, It enforced request validation and restrictions on certain operations, preventing us from directly passing the access details object in the request.
But what happened when we used__proto__
and how it updated the default role that were set in the application?
Let's take a look at the code for creating a employee
There is a default access Object set in the application
//At line 34 the code is calling Object.create(access)
let employeeData = Object.create(access);
As we looked at it in our previous post, the above line constructs the employeeData object based on the access object. Consequently, the employeeData object's prototype will reference the access object.
//In line 35 it calls Object.assign()
employeeData = Object.assign(employeeData,employee);
As we looked at it in our previous post, this method assigns the properties of employee to employeeData.
Let's Go Into Debug Mode:
The employee object is received as part of the request. Accessing __proto__
returns the accessDetails object that was included in the request.
When line 34 is executed, employeeData's prototype that references access Object get's the accessDetail Object as a result of Object.create(access) call. Below is what we get when we do employeeData.__proto__
When line 35 is executed it copies all the properties from employee to employeeData along with prototypes. below is what we get when we do employeeData.__proto__
after the Object.assign() call, this modifies the prototype of employeeData as the employee object contains __proto__
key.
When line 52,53 and 54 is executed, it initially verifies whether employeeData possesses its own accessDetails property. If not, it examines the prototype of employeeData and utilizes the value found there.
Why This Worked?
The employeeData was generated with the access object as its prototype using Object.create(access). As a result of this the employeeData got the accessDetails object in its prototype. When Object.assign(employeeData, employee) was invoked, it copied all properties from employee to employeeData, thereby overwriting the prototype value as well. This led to prototype pollution.
What should be the Fix For this Issue?
Create objects without prototypes: Object.create(null)
Instead of using Object literal {}, We can use the Object.create() and pass null as the first argument like Object.create(null), which will create an object without prototype and hence cannot be polluted.
Using Object.freeze()
This will make the object immune to any kind of updates to its prototype. We can also use Object.freeze(Object.prototype) to freeze the default/global prototype object and prevent it from pollution.
Using Third Party Libraries that are free from Prototype Pollution
Make sure we are scanning the third party libraries for prototype pollution and using the version that is free from this vulnerability.
Proper Request Validation
We can make a list of allowed keys that needs to be passed in our request and delete or throw appropriate error when we see some unwanted keys.
function validateObject(obj) {
const allowedProperties = ['key1', 'key2', 'key3'];
for (const key in obj) {
if (!allowedProperties.includes(key)) {
delete obj[key]; // Remove unwanted properties
}
}
}
Checking for
__proto__
while looping through Object
function copyObject(source) {
const target = {};
for (const key in source) {
if (key !== '__proto__') {
target[key] = source[key];
}
}
return target;
}
Check the equality of your Object's prototype
//req.body is also a object and it will also have its prototype
//this check can verify if there is any modification done to
//the base/default prototype object
if(this.req.body.__proto__ !== Object.prototype){
return false;
}
These are some of the Solutions/mitigation techniques that we can use to avoid prototype pollution in our application.
Check for proto property recursively
function sanitize(obj) {
if (typeof obj !== 'object' || obj === null) return;
// check if __proto__ is defined at the current object
if (Object.prototype.hasOwnProperty.call(obj, '__proto__')) {
delete obj.__proto__;
}
// recursively check for the presense of __proto__ in nested objects
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
sanitize(obj[key]);
}
}
}
I know this was a very long post but If you made it to the end then thankyou for taking out time to read this, I hope this was useful.