Closed jportner closed 1 year ago
Unfortunately, when you have frozen the global Object prototype, the
parseKeys()
function will throw a TypeError when inherited properties are present. Example:
Whoops, I wrote parseKeys()
here when I meant to write parseValues()
, that is what was throwing the error. I edited the PR description accordingly.
Note that node --use_strict
is NOT "strict mode", is something v8-specific, and should never, ever be used. The only proper strict mode is with 'use strict';
or in a class body, or an ES Module.
Note that
node --use_strict
is NOT "strict mode", is something v8-specific, and should never, ever be used. The only proper strict mode is with'use strict';
or in a class body, or an ES Module.
TIL, thanks for pointing this out!
Apparently the correct way to enable strict mode in the Node.js REPL is to use the NODE_REPL_MODE=strict
env var.
Patch coverage: 100.00
% and project coverage change: -0.80
:warning:
Comparison is base (
9dca37f
) 99.79% compared to head (7895b94
) 99.00%.
:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Do you have feedback about the report comment? Let us know in this issue.
node < 6 has a different error message; i fixed that locally with a regex, but node <= 0.8 actually doesn't throw when Object.prototype is frozen, so I'll make a package to detect that, and update this, and then land it.
Thank you!
Background
Click to expand
_Note: examples use the Node.js REPL with strict mode: `NODE_REPL_MODE=strict node`._ ### Inheritance and Shadowing Objects in JavaScript [inherit properties from their prototype chain](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_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: ```js 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](https://en.wikipedia.org/wiki/Variable_shadowing)" by the new property: ```js obj.toString = () => 'foo'; obj.toString(); // 'foo' Object.getOwnPropertyDescriptor(obj, 'toString'); // { value: [Function: toString], writable: true, enumerable: true, configurable: true } ``` ### Prototype Pollution [From Snyk:](https://learn.snyk.io/lessons/prototype-pollution/javascript/) > 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.)](https://www.npmjs.com/package/nopp) [From MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze): > The `Object.freeze()` static method *freezes* an object. Freezing an object [prevents extensions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/preventExtensions) 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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/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: ```js 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: ```js let obj2 = {}; obj2.toString = () => 'bar'; // Uncaught TypeError: Cannot assign to read only property 'toString' of object '#The Problem
The
allowPrototypes
option is designed to strip any query parameters if they would shadow/overwrite properties on the global Object prototype (for example, "toString"). This logic is in theparseKeys()
function:https://github.com/ljharb/qs/blob/9dca37f15de317fe9ad0ced907cdf250ba310880/lib/parse.js#L172-L177
However, before calling
parseKeys()
, the parse module first callsparseValues()
to create a temporary object:https://github.com/ljharb/qs/blob/9dca37f15de317fe9ad0ced907cdf250ba310880/lib/parse.js#L246-L256
Unfortunately, when you have frozen the global Object prototype, the
parseValues()
function will throw a TypeError when inherited properties are present. Example:This means that projects cannot use this package if they have frozen the global Object prototype.
The Solution
Since the
parseValues()
function returns a temporary object, the simple solution here is to create a "plain object" that does not have a prototype. This doesn't require any other logic changes, because the if theallowPrototypes
option is set to false, theparseKeys()
function will subsequently strip out any problematic properties.With this change, this package will be compatible with the "frozen prototype" approach of mitigating Prototype Pollution 🎉
Now you can use the same example as above, and it works as expected: