What is Prototype Pollution?

Asked

Viewed 237 times

10

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

  • What exactly is Prototype Pollution?
  • I can see if my code is susceptible without using external libraries?
  • How do I make to prevent my code from being vulnerable?

1 answer

9


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

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


Modifying __proto__

Pollution of prototypes (prototype Pollution), in summary, it happens when you modify the prototype of a value, which will reflect on the change of all other values that share the same prototype. Take an 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

In the above example, I used the __proto__ (present in all objects) to access and change the prototype of a given object. Currently, the language also offers other mechanisms to do this, such as Reflect.getPrototypeOf and Reflect.setPrototypeOf. By virtue of the fact that __proto__ be present in all objects of language, it becomes trivial to alter (often unintentionally) the prototype of a certain object.

Note that when 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.prototype (which is the prototype of the literal object we created).

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

This vulnerability is very common in merger operations (merge), cloning (clone), path assignment (path assignment) or extent (extend) object.

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

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

It is far from being 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 flag --disable-proto.


Extension of native objects (constructor augmentation)

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

For example, Javascript does not have the method Array.prototype.shuffle (to shuffle an array). With this, instead of creating an function shuffle, an attachment is attached new and non-standard method in the arrays prototype itself. Thus:

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 sound like a good idea, as it gets more syntactically pleasurable. It’s much nicer to do [1, 2, 3].shuffle than shuffle([1, 2, 3]) - at least many people seem to think so. However, this can have a number of negative consequences:

  1. Possible interference between codes

    In case this practice becomes common, there is no guarantee that an A library can extend or modify what a B library has already modified.

  2. Breach of long-term compatibility

    If too many people decide to extend a native object, two bad situations can emerge:

    • The specification standardize the new method and all the code you modified will be with the unofficial implementation, which is undoubtedly less performative.
    • The specification simply not being able to create a new method. This happened with the Array.prototype.contains (original name which, 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 the for..in to iterate on the properties of a certain object. As this loop also takes into account the properties inherited by the prototype (properties not proper), if one does not take the slightest care in extending it, it may affect the ties for..in. Behold:

    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]);
    }

    Because of this, in case of extending a native Javascript object, it is recommended to use the method Object.defineProperty to create the new property with the attribute [[Enumerable]] defined as false, not to change the standard behaviour of for..in. Learn more about descriptors and property attributes here.

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

Browser other questions tagged

You are not signed in. Login or sign up in order to post.