Tip
Prevent Global Prototype Mutability in JavaScript with SES lockdown
Understand the consequences of global mutability in JavaScript and how it can compromise the security and integrity of your application.
Here we have some JavaScript code that iterates over this test array and then just applies a makeFullName function to it.
const testArray = [
{
firstName: "Thomas",
lastName: "Greco",
username: "tgrecojs",
email: "tgl18509@pm.me"
},
{
firstName: "Stuart",
lastName: "Little",
username: "littleone",
email: "stu@hotmail.com"
},
{
firstName: "CeeDee",
lastName: "Lamb",
username: "ceedee88",
email: "clamb@cowboys.com"
}
];
const makeFullName = ({firstName, lastName}) => ({fullname: firstName.concat(' ', lastName)})
So if we map over testArray and pass in makeFullName we'll get back an array of objects with a fullname property.
const displayNames = testArray.map(makeFullName);
console.log(displayNames);
// => [
// { fullname: 'Thomas Greco' },
// { fullname: 'Stuart Little' },
// { fullname: 'CeeDee Lamb' }
// ]
Nothing complex is going on here and this is expected behavior. Below this functionality we have a group of log statements that are checking the global objects Object and Array to see if they are frozen.
console.group('##### Checking globals #####');
console.log('================================');
console.log('Object.prototype:::', Object.isFrozen(Object.prototype));
console.log('================================');
console.log('Array.prototype:::', Object.isFrozen(Array.prototype));
console.log('================================');
console.groupEnd();
// => ##### Checking globals #####
// ==========================
// Object.prototype::: false
// ==========================
// Array.prototype::: false
// ==========================
We know that prototypes of global objects are mutable, so we shouldn't be surprised to see each of these checks evaluating to false.
With this knowledge, let's engage in some mutating ourselves. Here, I'm just going to extend the Array.prototype and reimplement the map method.
Array.prototype.map = function(cb) {
let result = [];
for (let i = 0; i < this.length; i++) {
let current = this[i];
result.push(cb(current));
}
return result;
}
If you run this code you'll notice that nothing has changed. We get the same displayNames as we had before. We've achieved our desired outcome.
However, attackers can take advantage of this feature of JavaScript by changing/adding to the prototype method with any JavaScript code they want. This is known as prototype pollution.
For example, we could extend the map prototype to include a secretResult array that we add values to as we iterate through the array.
Array.prototype.map = function(cb) {
let result = [];
let secretResult = [];
for (let i = 0; i < this.length; i++) {
let current = this[i];
secretResult.push({current, transformed: cb(current)});
result.push(cb(current));
}
console.log(secretResult);
return result;
}
And now we can change the return statement so that it sends off our secretResult using a sendDataToSecretServer function before returning the result array.
Array.prototype.map = function(cb) {
let result = [];
let secretResult = [];
for (let i = 0; i < this.length; i++) {
let current = this[i];
secretResult.push({current, transformed: cb(current)});
result.push(cb(current));
}
return sendDataToSecretServer(secretResult) && result;
}
Now any sensitive data that a user could be exposing to our application is being sent to some secret server they don't know about.
To counter this type of behavior we need to lock down our JavaScript so that we can prevent global mutability.
This is where a library called SES comes in. What we can do is import ses into our project and invoke lockdown which does exactly what you'd expect, lock down our JavaScript so that we can prevent global mutability.
// I have it locally installed so I'm just importing it here
import './scripts/ses.umd.js';
lockdown();
Now when you run the code you'll get the following error:
TypeError: Cannot assign to read only property 'map' of object '[object Array]'
And finally just to show that the Object.prototype and Array.prototype are now frozen, we can revisit our log statements and see that they are now true.
console.group('##### Checking globals #####');
console.log('================================');
console.log('Object.prototype:::', Object.isFrozen(Object.prototype));
console.log('================================');
console.log('Array.prototype:::', Object.isFrozen(Array.prototype));
console.log('================================');
console.groupEnd();
// => ##### Checking globals #####
// ==========================
// Object.prototype::: true
// ==========================
// Array.prototype::: true
// ==========================