Every object in Javascript is built on top of a “prototype”. A prototype can either be either another object, or null. When an object’s prototype is another object, the first object can inherit fields from the second.

This type of prototypal inheritance is well-accepted and relatively well understood by most Javascript developers, but there are dangerous implications behind this kind of inheritance model.

Let’s dive into how we can hack prototypal inheritance for fun and profit!

How are Prototypes Created?

A Javascript object’s prototype is referenced through the hidden __proto__ field. This field can be found on every object in an application. The __proto__ field can either point to another object, or null if the current object has no prototype.

The two options for an object's prototype.

The prototype of an object can explicitly be set using Object.create, and passing in your desired prototype.


let p = { foo: 123 };
let a = Object.create(p);
let b = Object.create(null);

In this example, our new a object inherits the foo field from the p object being used as its prototype. Our new b object has no prototype, and inherits no fields.

Our two new objects and their prototype chains.

The prototype of an object can also be manually set through the __proto__ field:


let c = {};
c.__proto__ = { bar: 234 };

In this case, we replace the reference to c’s original prototype with a reference to a new object. We can now access the inherited bar field through c.

It's objects all the way down.

By default, all Javascript objects created through the literal notion point to Object.prototype as their prototype. Object.prototype is an object that holds helper functions like constructor, hasOwnProperty, and toString. Additionally, Object.prototype has a prototype of null.

This means that in addition to the bar field, our c object also has access to everything living in Object.prototype via its prototype’s prototype!

Setting the Scene

Armed with this information, let’s think about how we can exploit a simple (read: contrived) Node.js application.

Let’s assume that we’re building an application using an Express-like framework. We’ve created one endpoint to update values in an in-memory key-value store:


const store = {
    cats: "rule",
    dogs: "drool"
};

app.post('/update/:key/:value', function(req, res) {
    let { key, value } = req.params;
    res.send(_.set(store, key, value));
});

The /update route is used to update our store with various facts. This route is unauthorized as its intended to be used by unauthenticated clients.


We have another route, /restricted, that’s only intended to be used by authenticated, authorized users:


app.post('/restricted', function(req, res) {
    let user = getUser(req);
    if (!user || !user.isAdmin) {
        throw new Error("Not authorized!");
    }
    res.send("Permission granted!");
});

Let’s assume that the getUser function returns a user object based on a session token provided through req. Let’s also assume that the isAdmin field is set to true on administrator user objects, and unset on non-administrator user objects.

Hacking the Prototype

Now that the scene is set, imagine that we’re a normal, non-administrator, user of this application, and we want access to the /restricted endpoint.

Our calls to /restricted return a "Not authorized!" exception because our user object returned by getUser doesn’t have an isAdmin field. With no way of updating our admin flag, it seems we’re stuck.

Or are we?

Thankfully, our recent reading on prototypal inheritance has given us a flash of malevolent insight!

The /update endpoint is using Lodash’s _.set function to update the value of any field in our store, including nested fields. We can use this to our advantage. We quickly make a call to /update with a key of "__proto__.isAdmin", and a value of "true" (or any other truthy value), and try our restricted endpoint again:


Permission granted!

Victory! We’ve given ourself access to a restricted endpoint by modifying an arbitrary object within our Javascript application!

But how did we do it?

Explaining the Magic

As we mentioned earlier, unless specifically created with a different prototype, all objects reference Object.prototype as their prototype. More specifically, all objects in an application share the same reference to the same instance of Object.prototype in memory.

If we can modify Object.prototype, we can effectively modify the fields inherited by all of the objects in our application.

Our request to the /update endpoint, with a key of "__proto__.isAdmin", and a value of "true" effectively turned into this expression on our server:


_.set(store, "__proto__.isAdmin", "true")

This expression reaches into Object.prototype through the __proto__ field of our store and creates a new isAdmin field on that object with a value of "true". This change has far reaching consequences.

Everything is an admin!

After we update our “store”, every object that exists in our application now inherits an isAdmin field with a value of "true". This means that on retrieving our user object from getUser, it looks something like this:


{
  _id: 123,
  name: 'Pete',
  __proto__: {
    isAdmin: 'true',
    constructor: ...,
    hasOwnProperty: ...,
    toString: ...,
    ...
    __proto__: null
  }
}

Because our base user object has no isAdmin field, trying to access isAdmin on this object results in the isAdmin field from our underlying Object.prototype object to be returned. Object.prototype returns a value of "true", causing our server’s permission check to pass, and giving us access to juicy, restricted functionality.

In Reality

Obviously, this a fairly contrived example. In the real world, this type of vulnerability wouldn’t present itself in such a simple way. That said, this vulnerability does exist in the real world. When it rears its head, it’s often incredibly ugly. Adding unexpected fields to every object in your system can lead to disastrous results.

For example, imagine a vulnerability like this existing in a Meteor application. Once the underlying Object.prototype is updated with superfluous fields, our entire applications falls to pieces. Any queries made against our MongoDB collections fail catastrophically:

Exception while invoking method 'restricted' MongoError: 
  Failed to parse: { 
    find: "users", 
    filter: { 
      _id: "NktioYhaJMuKhbWQw", 
      isAdmin: "true" 
    }, 
    limit: 1, 
    isAdmin: "true" 
  }. Unrecognized field 'isAdmin'.

MongoDB fails to parse our query object with the added isAdmin fields, and throws an exception. Without being able to query our database, our application is dead in the water.

Fixing the Vulnerability & Final Thoughts

The fundamental fix for this issue is incredibly simple. Don’t trust user-provided data.

If a user is allowed to update a field on an object (or especially a nested field in an object), always whitelist the specific fields they’re allowed to touch. Never use user-provided data in a way that can deeply modify an object (any object) on the server.

If you’re interested in this kind of thing, I encourage you to check out my latest project, Secure Meteor! It’s an in-the-works guide designed to help you secure your past, present, and future Meteor applications. As a token of thanks for signing up, I’ll also send you a free Meteor security checklist!