big.js
big.js copied to clipboard
Make big.js work when the Object prototype is frozen
Background
Note: examples use the Node.js REPL with strict mode: node --use_strict
.
Inheritance and Shadowing
Objects in JavaScript inherit properties from their prototype chain. For example, the "toString" property can be accessed on all objects, but it doesn't actually exist on each object, it exists on the global Object prototype:
let obj = {};
obj.toString();
// '[object Object]'
Object.getOwnPropertyDescriptor(obj, 'toString');
// undefined
Object.getOwnPropertyDescriptor(Object.prototype, 'toString');
// { value: [Function: toString], writable: true, enumerable: false, configurable: true }
Under normal circumstances, you can assign a property to an object using the =
operator, and any property of the same name in the object's prototype chain will not be modified, but will be "shadowed" by the new property:
obj.toString = () => 'foo';
obj.toString();
// 'foo'
Object.getOwnPropertyDescriptor(obj, 'toString');
// { value: [Function: toString], writable: true, enumerable: true, configurable: true }
Prototype Pollution
Prototype pollution is an injection attack that targets JavaScript runtimes. With prototype pollution, an attacker might control the default values of an object's properties. This allows the attacker to tamper with the logic of the application and can also lead to denial of service or, in extreme cases, remote code execution.
There are a few different ways to mitigate Prototype Pollution, and one way to do it across the board is to freeze the global "root" objects and their prototypes (Object, Function, Array, etc.)
The
Object.freeze()
static method freezes an object. Freezing an object prevents extensions and makes existing properties non-writable and non-configurable. A frozen object can no longer be changed: new properties cannot be added, existing properties cannot be removed, their enumerability, configurability, writability, or value cannot be changed, and the object's prototype cannot be re-assigned.
This means that any attempt to change the Object prototype will fail. If using strict mode, it will throw an error; otherwise, it will be silently ignored.
If the Object prototype becomes frozen, all of its properties are no longer writable or configurable:
Object.freeze(Object.prototype);
Object.getOwnPropertyDescriptor(Object.prototype, 'toString');
// { value: [Function: toString], writable: false, enumerable: false, configurable: false }
This also prevents shadowing properties with assignment. If an object doesn't already have a property defined (such as "toString"), and it inherits a non-writable property of that name from its prototype chain, any attempt to assign the property on that object will fail:
let obj2 = {};
obj2.toString = () => 'bar';
// Uncaught TypeError: Cannot assign to read only property 'toString' of object '#<Object>'
obj2.toString();
// '[object Object]'
This behavior is described in the ECMAScript 2016 specification:
Assignment to an undeclared identifier or otherwise unresolvable reference does not create a property in the global object. When a simple assignment occurs within strict mode code, its LeftHandSideExpression must not evaluate to an unresolvable Reference. If it does a ReferenceError exception is thrown (6.2.3.2). The LeftHandSideExpression also may not be a reference to a data property with the attribute value {[[Writable]]: false}, to an accessor property with the attribute value {[[Set]]: undefined}, nor to a non-existent property of an object whose [[Extensible]] internal slot has the value false. In these cases a
TypeError
exception is thrown (12.15).
The Problem
Unfortunately, this package uses assignment to shadow the "constructor", "toString", and "valueOf" functions:
https://github.com/MikeMcl/big.js/blob/9c6c959c92dc9044a0f98c31f60322fd91243468/big.js#L112-L114
https://github.com/MikeMcl/big.js/blob/9c6c959c92dc9044a0f98c31f60322fd91243468/big.js#L963
https://github.com/MikeMcl/big.js/blob/9c6c959c92dc9044a0f98c31f60322fd91243468/big.js#L1014
This means that projects cannot use this package if they have frozen the global Object prototype.
The Solution
You can still shadow non-writable prototype properties by explicitly defining a new data property on the object:
Object.defineProperty(obj2, 'toString', { value: () => 'bar' });
obj2.toString();
// 'bar'
Object.getOwnPropertyDescriptor(obj2, 'toString');
// { value: [Function: toString], writable: false, enumerable: false, configurable: false }
The module can be changed to use this method of shadowing so it is compatible with this approach of mitigating Prototype Pollution 🎉
Thanks for the detailed analysis. I think this been raised here before, or perhaps at one of my other libs.
Can you say anything about how common it is for Object.prototype
to be frozen? Is this a real issue in user-land?
Thanks for the detailed analysis. I think this been raised here before, or perhaps at one of my other libs.
Sure thing!
Can you say anything about how common it is for
Object.prototype
to be frozen? Is this a real issue in user-land?
I'm not sure I can accurately estimate how common it is for Object.prototype
to be frozen. The only reason I can see for freezing Object.prototype
is to defend against Prototype Pollution, which can be extremely severe but is not well-known like other attacks (XSS, SQLi, etc.)
If I had to hazard a guess, I'd say it's probably fairly uncommon. Unfortunately it's very difficult to freeze global prototypes in this way for large JS applications, as a not-insignificant amount of 3rd-party packages like this one will break!
There are several ways to defend against Prototype Pollution, but the most consistent way to do it across the board is to freeze the global prototypes (namely Object.prototype
, Array.prototype
, and Function.prototype
). This is pretty standard guidance:
- https://learn.snyk.io/lessons/prototype-pollution/javascript/
- https://portswigger.net/web-security/prototype-pollution/preventing
- https://infosecwriteups.com/javascript-prototype-pollution-practice-of-finding-and-exploitation-f97284333b2
- https://book.hacktricks.xyz/pentesting-web/deserialization/nodejs-proto-prototype-pollution#what-can-i-do-to-prevent
For the application I'm working on, I'm using patch-package to patch dependencies piecemeal as I run into these issues, but it's something of a last resort.
@MikeMcl friendly bump on this, do you think this change can be merged?