Prototype Pollution

JavaScript is unique amongst mainstream programming languages in that it makes use of object-based inheritance. Rather than being instantiated from classes, most objects are associative arrays that inherit properties from an existing object (the prototype). Each object has a back-reference to the prototype object via the __proto__ property.

The presents an opportunity for attackers: if they can modify the prototype object, they can potentially inject code to all in-memory objects created in the same way.

Risks

Prevalence Common
Exploitability Moderate
Impact Harmful

If the attacker replaces a commonly called function on objects, they can execute whatever code they choose in that environment. This permits cross-site scripting attacks in the browser, or remote-code execution in Node.js applications.

How Prototype Pollution Works

Consider the following code taken from an early version of the express-fileupload module in Node.js:

function processNested(data) {
  if (!data || data.length < 1) return {}

  let d = {}, keys = Object.keys(data)

  for (let i = 0; i < keys.length; i++) {
    let key      = keys[i],
        value    = data[key],
        current  = d,
        keyParts = key
          .replace(new RegExp(/\[/g), '.')
          .replace(new RegExp(/\]/g), '')
          .split('.')

    for (let index = 0; index < keyParts.length; index++) {
      let k = keyParts[index]
      if (index >= keyParts.length - 1){
        current[k] = value
      } else {
        if (!current[k]) current[k] = !isNaN(keyParts[index + 1]) ? [] : {}
        current = current[k]
      }
    }
  }

  return d
}

This function is designed to “unfold” meta-data object like:

{
  "a.b.c" : "value"
}

…into a nested object with the following form:

{
  "a" : {
    "b" : {
      "c" : "value" 
    }      
  }  
}

However, the code is way too permissive - it allows fields to be set on the built-in __proto__ object in the following manner:

let payload = JSON.parse(
  '{ "__proto__.injected" : "This variable exists on all objects" }'
)

processNested(payload)

// This will print "This variable exists on all objects", since 
// we have injected a property in the global namespace.
console.log(injected)

// This will print "This variable exists on all objects", since 
// all new objects will have this property too!
console.log(Object().injected)

Mitigation

To mitigate prototype pollution attacks, make sure you explicitly enumerate the properties you set on objects in response to user actions. In particular, be sure not to set overwrite any internal properties that begin with the _ character. When dealing with nested objects, make assertions in your code about the types of objects you are dealing with as you pull them from properties.

There are a number of other coding practices that will help avoid prototype pollution vulnerabilities.

Freeze Your Objects

You can make your objects immutable using the freeze() method:

const obj = {
  prop: 42
}

// Make the object immutable.
Object.freeze(obj)

// The object has become frozen, so this will return true.
Object.isFrozen(obj) 

// Attempts to modify the object throw an error.
obj.prop = 33

Freezing an object also prevents its prototype from being changed.

Use Prototypeless Object

JavaScript objects can be created without any prototype. Objects created from Object.create(null) won’t have __proto__ and constructor attributes. In this way, the object prototype will never be polluted.

Use Maps Instead of Objects

The Map primitive was introduced in ES6. The Map data structure stores key/value pairs, and it is not susceptible to prototype pollution.

const map1 = new Map();

map1.set('a', 1);
map1.set('b', 2);
map1.set('c', 3);

// Will print "1"
console.log(map1.get('a'));

Further Considerations

Prototype pollution vulnerabilities often occur in third-party JavaScript libraries, so make sure to keep ahead of security advisories periodically running the npm audit tool if you are developing in Node.js.

Further Reading