What is Prototype Pollution?

I use a tool that performs security checks on the packages of my project, it indicated to me that one of the packages is susceptible to Prototype Pollution, I would like to know:

  • what exactly is Prototype Pollution?
  • I can check if my code is susceptible without using external libraries?
  • How do I prevent my code from being vulnerable?
Author: Luiz Felipe, 2020-05-04

1 answers

First of all, it is worth minimally understanding what prototypes are in JavaScript. Basically, every object in JavaScript can inherit properties and methods from the prototype chain.

Primitives, in turn, are not objects and therefore have no properties, but in the face of something called "primitive wrapping", they have associations with the corresponding objects. For example, the primitive string is wrapped by the constructor String; primitive number, by Number and so on. That is why, although primitives do not have properties, you can in practice access properties in "primitive values".


Modifying __proto__

The prototype pollution attack (prototype pollution), in short, happens when you modify the prototype of a value, which will reflect the change of all other values that share the same prototype. See a example:

const obj1 = { name: 'Foo' };
const obj2 = { name: 'Bar' };

console.log(obj1.changed); // undefined
console.log(obj2.changed); // undefined

obj1.__proto__.changed = true;

console.log(obj1.changed); // true
console.log(obj2.changed); // true

const obj3 = { name: 'Baz' };
const str1 = 'Qux';

console.log(obj3.changed); // true
console.log(str1.changed); // true

while not something standardized by the language specification, the overwhelming majority of ECMAScript implementations provide a general property __proto__ (present in all objects) to access and change the prototype of a given object. Currently, the language also offers mechanisms to do this, such as Reflect.getPrototypeOf and Reflect.setPrototypeOf. By virtue of __proto__, present in all objects, it becomes trivial to alter (often intentionally) the prototype of a certain object.

Note that by changing the object __proto__, you change is changing the prototype of the object that contains it. In the example above, we are modifying the object Object (which is the prototype of the literal object we created). This means that all the values that inherit it will have been polluted because.

Also note that since prototypes form a string, when modifying Object, virtually all JavaScript values will also have been invaded, since pretty much everything extends Object (except null and undefined). This happens because, unlike primitives, in JavaScript, objects are passed by reference . Thus, since the prototype of a given object is often shared (by reference) among several other objects ("instances"), you will probably end up modifying something that was not meant to be modified, since the changes in this case are not local, but rather global.

It vulnerability is very common in Merge (merge), clone (clone), path assignment (path assignment) or extension (extend) operations of objects.

Note that, <obj>.__proto__ allows direct access to the prototype property of the constructor of a given object. You can also access (and modify) it via <obj>.constructor.prototype. Learn more about the property constructor here. To state, unlike __proto__, the property construtor it is specified .

If you are working with objects dynamically, then you should ensure that you modify the __proto__ or constructor property by mistake, which can be used as a means for this type of attack. You can also use the hasOwnProperty, to ensure that you are not using legacy properties.

it is far from a good solution to the problem, but it is interesting to comment that there is how to disable or prevent the use of __proto__ in Node.js through the flag --disable-proto.


Native object extension (augmentation constructor )

Another very common form of prototype pollution is to modify the prototype property of a constructor. In this case it can also be called prototype augmentation, since it is intentional.

For example, JavaScript does not have the Array.prototype.shuffle method (for shuffling an array). With this, instead of creating a function shuffle, a new and non-standard method is attached to the arrays prototype itself. Like this:

Object.defineProperty(Array.prototype, 'shuffle', {
  writable: true,
  configurable: true,
  enumerable: false,
  value: function() {
    // Implementação qualquer.
    console.log('Chamado Array.prototype.shuffle em:', this);
  }
});

[1, 2, 3].shuffle();

At first, this might seem like a good idea, since it gets syntactically nicer. It's much nicer to do [1, 2, 3].shuffle than shuffle([1, 2, 3]) - at least a lot of people seem to find it. However, this can bring a number of negative consequences:

  1. Possible interference between codes

    No if this practice becomes common, there is no guarantee that a library A will be able to extend or modify what a library B has already modified.

  2. Long-term compatibility break

    If a lot of people decide to extend a native object, two bad situations can emerge:

    • the specification standardize the new method and all the code it modified will stay with the unofficial implementation, which is undoubtedly less performatic.
    • the specification simply not be able to create a new method. This happened with Array.prototype.contains (original name that, in order not to break compatibility, had to be changed to Array.prototype.includes). [Ref]
  3. Change the result of for..in

    Many people use for..in to iterate over the properties of a certain object. Since this loop also takes into account the properties inherited by the prototype (properties not own), if the person does not take the slightest care when extending it, it may affect the loops for..in. See:

    const arr = ['a', 'b'];
    
    // Antes de estender `Array.prototype`:
    for (const key in arr) {
      console.log(key, '->', arr[key]);
    }
    
    console.log('----');
    
    // Estendendo o protótipo de `Array` (forma leviana):
    Array.prototype.shuffle = function() {
      // Implementação qualquer.
      console.log('Chamado Array.prototype.shuffle em:', this);
    }
    
    // Depois da extensão.
    for (const key in arr) {
      console.log(key, '->', arr[key]);
    }

    For this reason, in case of extending a native JavaScript object, it is recommended to use the Object.defineProperty method to create the new property with the [[Enumerable]] attribute set to false, in order not to change the default behavior of for..in. learn more about property descriptors and attributes here.

In in short, only modify the code if you are sure of what you are doing. On a small scale, the problem will not be so alarming, but in these conditions it is worth asking: Is it really worth it? Most of the time one function is enough.

 5
Author: Luiz Felipe, 2021-02-03 16:46:09