What is the fuss with __proto__ in Node?
So, lately, I have been looking into some internals of Node (and JavaScript in general) and I came across an interesting security problem: prototype pollution attack. For some reason, it seems hard to understand but let’s try to give a reasonably simple explanation:
First, we create a class in Node:
class Animal {
#species
constructor (species) {
this.#species = species
}
sayHello() {
console.log(`Hello, I am a ${this.#species}`)
}
}Pretty standard. You dont need to be a genius of programming to understand it. For those who don’t have time to catch up with the world, # represents a private attribute (I know, the same happened to me a few weeks back).
If you are coming from an object oriented language like Java, the above code feels almost familiar. As we don’t want to sit back and relax, lets visit an equivalent way of writing the same class:
function FunctionalAnimal (species) {
this._species = species
this.sayHello = function () {
console.log(`Hello I am a ${this._species}`)
}
}This is a bit more… well, alien.
The resutl is the same: a bucket that contains data + methods to operate that data. There is one small difference though: the private attribute cannot be private as the # symbol was introduced in JavaScript for classes so we use a convention (a non written rule) specifying that elements starting with underscore are private.
A function that emulates a class is a big problem for me: coding is complex enough to make our life even more complicated with artificial constructs, but up to a couple of years ago, JavaScript did not support classess natively. That forced developers to come with “interesting” ways of modelling classes.
After all, classess are just syntactic sugar for functions(or so the Internet says, I believe they bring more things than just “hiding the pain”) but the reality is that class encapsulation is a high cognitive tool used by developers to model real world artifacts. A computer has no concept of classess, just instructions and data.
Now it is time for things to go weird:
const animal = new Animal('fish')
animal['sayHello']()
animal.sayHello()The above code works and does what you thing it does: calls the method sayHello twice.
This is just how JavaScript works and the best thing you can do is assume it and learn its caviats. One of them is that flexibilty, brings problems.
That flexibility allso allow us to do things like:
const animal = new Animal('fish')
animal['test'] = () => { console.log('this is awkward') }
animal.test()Indeed. It is awkward: you can define properties on the fly. That would’t be a problem if it wasn’t because there is a special property, __proto__ that allows you to rewrite the prototype of an object:
const animal = new Animal('fish')
const functionalAnimal = new FunctionalAnimal('fish')a = {}a.__proto__.test = () => { console.log('this should not happen') }animal.test()
functionalAnimal.test()What did just happened there? Pretty easy: the prototype of the type Object in JavaScript has been modified and we can test that by checking what is the signature of the Object.prototype:
console.log(Object.prototype)Spot on, the prototype of Object now contains a method called test. Every single object (created or to be created) now contains a method at the prototype level that is called test which, will be invoked if the instance of the object does not shadow it by redefining the same.
This can happen not just with the Object.prototype but in any prototype.
If we are parsing JSON that contains data entered by a user, we have the not so perfect storm forming:
let parsedJSON = JSON.parse('{"__proto__": {"checkIfICanDo" : true}}')
console.log(parsedJSON.checkIfICanDo)let myVar = Object.assign({}, parsedJSON)
console.log(myVar.checkIfICanDo)In any JavaScript object, by default, __proto__ is a getter/setter. When a JSON that contains a property called __proto__ is parsed, the getter/setter is overriden hence there is no prototype modification.
The problems comes when Object.assign is executed: when copying the attribute __proto__, the __proto__ setter of the target is called and the prototype is altered leading into a property called checkIfICanDo that was injected via JSON.
This, could allow an attacker to define properties leading into a number of undesirable side effects or vulnerabilities.
