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:
-
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.
-
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]
-
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.
Related: How prototypes work in Javascript?
– Denis Rudnei de Souza