Here we have some JavaScript code that iterates over this test array and then just applies a makeFullName
function to it.
1const testArray = [
2 {
3 firstName: "Thomas",
4 lastName: "Greco",
5 username: "tgrecojs",
6 email: "tgl18509@pm.me"
7 },
8 {
9 firstName: "Stuart",
10 lastName: "Little",
11 username: "littleone",
12 email: "stu@hotmail.com"
13 },
14 {
15 firstName: "CeeDee",
16 lastName: "Lamb",
17 username: "ceedee88",
18 email: "clamb@cowboys.com"
19 }
20];
21
22const 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.
1const displayNames = testArray.map(makeFullName);
2
3console.log(displayNames);
4// => [
5// { fullname: 'Thomas Greco' },
6// { fullname: 'Stuart Little' },
7// { fullname: 'CeeDee Lamb' }
8// ]
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.
1console.group('##### Checking globals #####');
2console.log('================================');
3console.log('Object.prototype:::', Object.isFrozen(Object.prototype));
4console.log('================================');
5console.log('Array.prototype:::', Object.isFrozen(Array.prototype));
6console.log('================================');
7console.groupEnd();
8// => ##### Checking globals #####
9// ==========================
10// Object.prototype::: false
11// ==========================
12// Array.prototype::: false
13// ==========================
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.
1Array.prototype.map = function(cb) {
2 let result = [];
3 for (let i = 0; i < this.length; i++) {
4 let current = this[i];
5 result.push(cb(current));
6 }
7 return result;
8}
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.
1Array.prototype.map = function(cb) {
2 let result = [];
3 let secretResult = [];
4 for (let i = 0; i < this.length; i++) {
5 let current = this[i];
6 secretResult.push({current, transformed: cb(current)});
7 result.push(cb(current));
8 }
9 console.log(secretResult);
10 return result;
11}
And now we can change the return statement so that it sends off our secretResult
using a sendDataToSecretServer
function before returning the result
array.
1Array.prototype.map = function(cb) {
2 let result = [];
3 let secretResult = [];
4 for (let i = 0; i < this.length; i++) {
5 let current = this[i];
6 secretResult.push({current, transformed: cb(current)});
7 result.push(cb(current));
8 }
9 return sendDataToSecretServer(secretResult) && result;
10}
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.
1// I have it locally installed so I'm just importing it here
2import './scripts/ses.umd.js';
3lockdown();
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
.
1console.group('##### Checking globals #####');
2console.log('================================');
3console.log('Object.prototype:::', Object.isFrozen(Object.prototype));
4console.log('================================');
5console.log('Array.prototype:::', Object.isFrozen(Array.prototype));
6console.log('================================');
7console.groupEnd();
8// => ##### Checking globals #####
9// ==========================
10// Object.prototype::: true
11// ==========================
12// Array.prototype::: true
13// ==========================
Transcript
Here we have some JavaScript code that iterates over this test array and then just applies this makeFullName function to it. The newly created array, we're just going to have one key, FullName.
And this is created by concatenating this firstName and lastName property. So if we look at our console, we'll see that displayNames is displaying this array for us.
And then below displayNames, we see we have a group of log statements. And within this checking globals group, we're seeing the result of evaluating objects.isFrozen on the object and array prototype.
Seeing as JavaScript allows developers to mutate global variables, we shouldn't be surprised to see each of these checks evaluating to false. And on that note, let's engage in some mutating ourselves.
Here, I'm just going to extend the array prototype and reimplement the map method. So inside our function body, first we're going to initialize an empty array result.
Next, we'll introduce our for loop that is going to iterate through every value in the array map is called on. So in the context of our function, the this keyword refers to our source array.
So when setting the condition of our loop, we'll constrain i to be less than this.length.
And then finally, we're going to use the increment operator as our loop's afterthought so that i will continue to grow up until it is equal to this.length.
Inside the body of our loop, we're going to create a variable current. And using bracket notation, we'll just have this hold each element as we process the array. Directly below this, we'll use the push method to populate this result array.
And here is where we're going to use our function's callback argument or CB as I've named it. And the last thing we'll do is just return this result array.
Now if we save and look into our terminal, we'll see that our behavior has not changed at all. We get the same display names as we had before. We've achieved our desired outcome. And this is great. It's all sunshine and rainbows for us. Well, until it isn't.
So the property of global mutability can cause the occurrence of prototype. This occurs when an attacker takes advantage of JavaScript's mute ability to pollute a prototype for the purpose of executing some sort of attack.
To demonstrate this, I'm going to extend our map implementation so that it includes a second array. Secret result. As we loop through our code, secret result will be updated with the current element of the array as well as the newly transformed value.
Now we can log out secret result and we'll see that we've successfully captured these values. We certainly don't want to show our hand here, so let's remove this log statement.
And from here, we're going to grab this send data to secret server function. And now I'm going to change the return statement here so that it sends off our secret result using this send data to secret server function.
Before returning the result array. Now we'll see that the display name is showing just like it was before, giving the user a false sense of confidence. Now we do have some logging going on, so we'll remove that.
Okay, so now at the bottom of our log statements, I'm going to add an additional log. And here, I'm just going to use this secret database, which is just a JavaScript map.
And what I'm going to do here is just get this data set one key, which is going to open up this secret database.
Which is going to hold the data we've just sneakily gained access to. And I'll just name this first stolen data set. Okay, and we see that we have our array in here and I can just map over this array.
And let's just quickly inspect the current and transform the values. And just like that, we've been able to extract information about Thomas Greco, C.D. Lamb, and Stuart Little without much work.
On a more serious note, this can lead to extremely big problems. So, it's important that we remove any instances of global mutability whenever possible. Let's see how we can use the SESS library to accomplish this.
So, atop the page, I'm just importing SESS, as I have it locally within my project. And right below that, I'm invoking the lockdown method.
And as its name suggests, lockdown is going to lock down our JavaScript. The result, no more global mutability. So, if we save our file and we take a look in our console, we shouldn't be surprised to see that we now get an error.
Specifically, this error is telling us that this property is not configurable. It's read-only. Okay, so, let's remove this custom map implementation.
Now, if we look in our console, we'll see that display names is showing correctly. But, we still have an error at the bottom, and that's because first stolen dataset no longer exists. We've prevented this from ever being created.
The last thing we want to feast our eyes on is just the fact that the object prototype, as well as the array prototype, is now frozen by default. All we had to do was invoke one method, lockdown.
And in return, we've received a JavaScript environment in which all globals are now immutable.