Skip to content

Commit 1d09407

Browse files
aduh95targos
authored andcommitted
tools: add avoid-prototype-pollution lint rule
PR-URL: #43308 Backport-PR-URL: #44081 Reviewed-By: Rich Trott <[email protected]>
1 parent 0046b9a commit 1d09407

File tree

9 files changed

+258
-5
lines changed

9 files changed

+258
-5
lines changed

lib/.eslintrc.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ rules:
116116
- name: SubtleCrypto
117117
message: Use `const { SubtleCrypto } = require('internal/crypto/webcrypto');` instead of the global.
118118
# Custom rules in tools/eslint-rules
119+
node-core/avoid-prototype-pollution: error
119120
node-core/lowercase-name-for-primitive: error
120121
node-core/non-ascii-character: error
121122
node-core/no-array-destructuring: error

lib/crypto.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ ObjectDefineProperties(module.exports, {
297297
// Aliases for randomBytes are deprecated.
298298
// The ecosystem needs those to exist for backwards compatibility.
299299
prng: {
300+
__proto__: null,
300301
enumerable: false,
301302
configurable: true,
302303
writable: true,
@@ -305,6 +306,7 @@ ObjectDefineProperties(module.exports, {
305306
randomBytes
306307
},
307308
pseudoRandomBytes: {
309+
__proto__: null,
308310
enumerable: false,
309311
configurable: true,
310312
writable: true,
@@ -314,6 +316,7 @@ ObjectDefineProperties(module.exports, {
314316
randomBytes
315317
},
316318
rng: {
319+
__proto__: null,
317320
enumerable: false,
318321
configurable: true,
319322
writable: true,

lib/internal/bootstrap/node.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,7 @@ function createGlobalConsole(consoleFromVM) {
496496
// https://heycam.github.io/webidl/#es-namespaces
497497
function exposeNamespace(target, name, namespaceObject) {
498498
ObjectDefineProperty(target, name, {
499+
__proto__: null,
499500
writable: true,
500501
enumerable: false,
501502
configurable: true,
@@ -506,6 +507,7 @@ function exposeNamespace(target, name, namespaceObject) {
506507
// https://heycam.github.io/webidl/#es-interfaces
507508
function exposeInterface(target, name, interfaceObject) {
508509
ObjectDefineProperty(target, name, {
510+
__proto__: null,
509511
writable: true,
510512
enumerable: false,
511513
configurable: true,
@@ -516,6 +518,7 @@ function exposeInterface(target, name, interfaceObject) {
516518
// https://heycam.github.io/webidl/#define-the-operations
517519
function defineOperation(target, name, method) {
518520
ObjectDefineProperty(target, name, {
521+
__proto__: null,
519522
writable: true,
520523
enumerable: true,
521524
configurable: true,
@@ -526,6 +529,7 @@ function defineOperation(target, name, method) {
526529
// https://heycam.github.io/webidl/#Replaceable
527530
function defineReplacableAttribute(target, name, value) {
528531
ObjectDefineProperty(target, name, {
532+
__proto__: null,
529533
writable: true,
530534
enumerable: true,
531535
configurable: true,

lib/internal/bootstrap/pre_execution.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,18 +209,21 @@ function setupWebCrypto() {
209209
if (internalBinding('config').hasOpenSSL) {
210210
webcrypto ??= require('internal/crypto/webcrypto');
211211
ObjectDefineProperty(globalThis, 'Crypto', {
212+
__proto__: null,
212213
writable: true,
213214
enumerable: false,
214215
configurable: true,
215216
value: webcrypto.Crypto
216217
});
217218
ObjectDefineProperty(globalThis, 'CryptoKey', {
219+
__proto__: null,
218220
writable: true,
219221
enumerable: false,
220222
configurable: true,
221223
value: webcrypto.CryptoKey
222224
});
223225
ObjectDefineProperty(globalThis, 'SubtleCrypto', {
226+
__proto__: null,
224227
writable: true,
225228
enumerable: false,
226229
configurable: true,

lib/internal/per_context/domexception.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ function throwInvalidThisError(Base, type) {
1818
const key = 'ERR_INVALID_THIS';
1919
ObjectDefineProperties(err, {
2020
message: {
21+
__proto__: null,
2122
value: `Value of "this" must be of ${type}`,
2223
enumerable: false,
2324
writable: true,
2425
configurable: true,
2526
},
2627
toString: {
28+
__proto__: null,
2729
value() {
2830
return `${this.name} [${key}]: ${this.message}`;
2931
},

lib/internal/per_context/primordials.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ function copyPropsRenamed(src, dest, prefix) {
7878
copyAccessor(dest, prefix, newKey, desc);
7979
} else {
8080
const name = `${prefix}${newKey}`;
81-
ReflectDefineProperty(dest, name, desc);
81+
ReflectDefineProperty(dest, name, { __proto__: null, ...desc });
8282
if (varargsMethods.includes(name)) {
8383
ReflectDefineProperty(dest, `${name}Apply`, {
8484
__proto__: null,
@@ -105,7 +105,7 @@ function copyPropsRenamedBound(src, dest, prefix) {
105105
}
106106

107107
const name = `${prefix}${newKey}`;
108-
ReflectDefineProperty(dest, name, desc);
108+
ReflectDefineProperty(dest, name, { __proto__: null, ...desc });
109109
if (varargsMethods.includes(name)) {
110110
ReflectDefineProperty(dest, `${name}Apply`, {
111111
__proto__: null,
@@ -129,7 +129,7 @@ function copyPrototype(src, dest, prefix) {
129129
}
130130

131131
const name = `${prefix}${newKey}`;
132-
ReflectDefineProperty(dest, name, desc);
132+
ReflectDefineProperty(dest, name, { __proto__: null, ...desc });
133133
if (varargsMethods.includes(name)) {
134134
ReflectDefineProperty(dest, `${name}Apply`, {
135135
__proto__: null,
@@ -313,7 +313,7 @@ const copyProps = (src, dest) => {
313313
ReflectDefineProperty(
314314
dest,
315315
key,
316-
ReflectGetOwnPropertyDescriptor(src, key));
316+
{ __proto__: null, ...ReflectGetOwnPropertyDescriptor(src, key) });
317317
}
318318
});
319319
};
@@ -341,7 +341,7 @@ const makeSafe = (unsafe, safe) => {
341341
return new SafeIterator(this);
342342
};
343343
}
344-
ReflectDefineProperty(safe.prototype, key, desc);
344+
ReflectDefineProperty(safe.prototype, key, { __proto__: null, ...desc });
345345
}
346346
});
347347
} else {

lib/readline.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,7 @@ ObjectSetPrototypeOf(Interface.prototype, EventEmitter.prototype);
368368
ObjectSetPrototypeOf(Interface, EventEmitter);
369369

370370
ObjectDefineProperty(Interface.prototype, 'columns', {
371+
__proto__: null,
371372
configurable: true,
372373
enumerable: true,
373374
get: function() {
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
if ((!common.hasCrypto) || (!common.hasIntl)) {
5+
common.skip('ESLint tests require crypto and Intl');
6+
}
7+
8+
common.skipIfEslintMissing();
9+
10+
const RuleTester = require('../../tools/node_modules/eslint').RuleTester;
11+
const rule = require('../../tools/eslint-rules/avoid-prototype-pollution');
12+
13+
new RuleTester({
14+
parserOptions: { ecmaVersion: 2022 },
15+
})
16+
.run('property-descriptor-no-prototype-pollution', rule, {
17+
valid: [
18+
'ObjectDefineProperties({}, {})',
19+
'ObjectCreate(null, {})',
20+
'ObjectDefineProperties({}, { key })',
21+
'ObjectCreate(null, { key })',
22+
'ObjectDefineProperties({}, { ...spread })',
23+
'ObjectCreate(null, { ...spread })',
24+
'ObjectDefineProperties({}, { key: valueDescriptor })',
25+
'ObjectCreate(null, { key: valueDescriptor })',
26+
'ObjectDefineProperties({}, { key: { ...{}, __proto__: null } })',
27+
'ObjectCreate(null, { key: { ...{}, __proto__: null } })',
28+
'ObjectDefineProperties({}, { key: { __proto__: null } })',
29+
'ObjectCreate(null, { key: { __proto__: null } })',
30+
'ObjectDefineProperties({}, { key: { __proto__: null, enumerable: true } })',
31+
'ObjectCreate(null, { key: { __proto__: null, enumerable: true } })',
32+
'ObjectDefineProperties({}, { key: { "__proto__": null } })',
33+
'ObjectCreate(null, { key: { "__proto__": null } })',
34+
'ObjectDefineProperties({}, { key: { \'__proto__\': null } })',
35+
'ObjectCreate(null, { key: { \'__proto__\': null } })',
36+
'ObjectDefineProperty({}, "key", ObjectCreate(null))',
37+
'ReflectDefineProperty({}, "key", ObjectCreate(null))',
38+
'ObjectDefineProperty({}, "key", valueDescriptor)',
39+
'ReflectDefineProperty({}, "key", valueDescriptor)',
40+
'ObjectDefineProperty({}, "key", { __proto__: null })',
41+
'ReflectDefineProperty({}, "key", { __proto__: null })',
42+
'ObjectDefineProperty({}, "key", { __proto__: null, enumerable: true })',
43+
'ReflectDefineProperty({}, "key", { __proto__: null, enumerable: true })',
44+
'ObjectDefineProperty({}, "key", { "__proto__": null })',
45+
'ReflectDefineProperty({}, "key", { "__proto__": null })',
46+
'ObjectDefineProperty({}, "key", { \'__proto__\': null })',
47+
'ReflectDefineProperty({}, "key", { \'__proto__\': null })',
48+
],
49+
invalid: [
50+
{
51+
code: 'ObjectDefineProperties({}, ObjectGetOwnPropertyDescriptors({}))',
52+
errors: [{ message: /prototype pollution/ }],
53+
},
54+
{
55+
code: 'ObjectCreate(null, ObjectGetOwnPropertyDescriptors({}))',
56+
errors: [{ message: /prototype pollution/ }],
57+
},
58+
{
59+
code: 'ObjectDefineProperties({}, { key: {} })',
60+
errors: [{ message: /null-prototype/ }],
61+
},
62+
{
63+
code: 'ObjectCreate(null, { key: {} })',
64+
errors: [{ message: /null-prototype/ }],
65+
},
66+
{
67+
code: 'ObjectDefineProperties({}, { key: { [void 0]: { ...{ __proto__: null } } } })',
68+
errors: [{ message: /null-prototype/ }],
69+
},
70+
{
71+
code: 'ObjectCreate(null, { key: { [void 0]: { ...{ __proto__: null } } } })',
72+
errors: [{ message: /null-prototype/ }],
73+
},
74+
{
75+
code: 'ObjectDefineProperties({}, { key: { __proto__: Object.prototype } })',
76+
errors: [{ message: /null-prototype/ }],
77+
},
78+
{
79+
code: 'ObjectCreate(null, { key: { __proto__: Object.prototype } })',
80+
errors: [{ message: /null-prototype/ }],
81+
},
82+
{
83+
code: 'ObjectDefineProperties({}, { key: { [`__proto__`]: null } })',
84+
errors: [{ message: /null-prototype/ }],
85+
},
86+
{
87+
code: 'ObjectCreate(null, { key: { [`__proto__`]: null } })',
88+
errors: [{ message: /null-prototype/ }],
89+
},
90+
{
91+
code: 'ObjectDefineProperties({}, { key: { enumerable: true } })',
92+
errors: [{ message: /null-prototype/ }],
93+
},
94+
{
95+
code: 'ObjectCreate(null, { key: { enumerable: true } })',
96+
errors: [{ message: /null-prototype/ }],
97+
},
98+
{
99+
code: 'ObjectDefineProperty({}, "key", {})',
100+
errors: [{ message: /null-prototype/ }],
101+
},
102+
{
103+
code: 'ReflectDefineProperty({}, "key", {})',
104+
errors: [{ message: /null-prototype/ }],
105+
},
106+
{
107+
code: 'ObjectDefineProperty({}, "key", ObjectGetOwnPropertyDescriptor({}, "key"))',
108+
errors: [{ message: /prototype pollution/ }],
109+
},
110+
{
111+
code: 'ReflectDefineProperty({}, "key", ObjectGetOwnPropertyDescriptor({}, "key"))',
112+
errors: [{ message: /prototype pollution/ }],
113+
},
114+
{
115+
code: 'ObjectDefineProperty({}, "key", ReflectGetOwnPropertyDescriptor({}, "key"))',
116+
errors: [{ message: /prototype pollution/ }],
117+
},
118+
{
119+
code: 'ReflectDefineProperty({}, "key", ReflectGetOwnPropertyDescriptor({}, "key"))',
120+
errors: [{ message: /prototype pollution/ }],
121+
},
122+
{
123+
code: 'ObjectDefineProperty({}, "key", { __proto__: Object.prototype })',
124+
errors: [{ message: /null-prototype/ }],
125+
},
126+
{
127+
code: 'ReflectDefineProperty({}, "key", { __proto__: Object.prototype })',
128+
errors: [{ message: /null-prototype/ }],
129+
},
130+
{
131+
code: 'ObjectDefineProperty({}, "key", { [`__proto__`]: null })',
132+
errors: [{ message: /null-prototype/ }],
133+
},
134+
{
135+
code: 'ReflectDefineProperty({}, "key", { [`__proto__`]: null })',
136+
errors: [{ message: /null-prototype/ }],
137+
},
138+
{
139+
code: 'ObjectDefineProperty({}, "key", { enumerable: true })',
140+
errors: [{ message: /null-prototype/ }],
141+
},
142+
{
143+
code: 'ReflectDefineProperty({}, "key", { enumerable: true })',
144+
errors: [{ message: /null-prototype/ }],
145+
},
146+
]
147+
});
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
'use strict';
2+
3+
function checkProperties(context, node) {
4+
if (
5+
node.type === 'CallExpression' &&
6+
node.callee.name === 'ObjectGetOwnPropertyDescriptors'
7+
) {
8+
context.report({
9+
node,
10+
message:
11+
'Property descriptors inherits from the Object prototype, therefore are subject to prototype pollution',
12+
});
13+
}
14+
if (node.type !== 'ObjectExpression') return;
15+
for (const { key, value } of node.properties) {
16+
if (
17+
key != null && value != null &&
18+
!(key.type === 'Identifier' && key.name === '__proto__') &&
19+
!(key.type === 'Literal' && key.value === '__proto__')
20+
) {
21+
checkPropertyDescriptor(context, value);
22+
}
23+
}
24+
}
25+
26+
function checkPropertyDescriptor(context, node) {
27+
if (
28+
node.type === 'CallExpression' &&
29+
(node.callee.name === 'ObjectGetOwnPropertyDescriptor' ||
30+
node.callee.name === 'ReflectGetOwnPropertyDescriptor')
31+
) {
32+
context.report({
33+
node,
34+
message:
35+
'Property descriptors inherits from the Object prototype, therefore are subject to prototype pollution',
36+
suggest: [{
37+
desc: 'Wrap the property descriptor in a null-prototype object',
38+
fix(fixer) {
39+
return [
40+
fixer.insertTextBefore(node, '{ __proto__: null,...'),
41+
fixer.insertTextAfter(node, ' }'),
42+
];
43+
},
44+
}],
45+
});
46+
}
47+
if (node.type !== 'ObjectExpression') return;
48+
49+
for (const { key, value } of node.properties) {
50+
if (
51+
key != null && value != null &&
52+
((key.type === 'Identifier' && key.name === '__proto__') ||
53+
(key.type === 'Literal' && key.value === '__proto__')) &&
54+
value.type === 'Literal' && value.value === null
55+
) {
56+
return true;
57+
}
58+
}
59+
60+
context.report({
61+
node,
62+
message: 'Must use null-prototype object for property descriptors',
63+
});
64+
}
65+
66+
const CallExpression = 'ExpressionStatement[expression.type="CallExpression"]';
67+
module.exports = {
68+
meta: { hasSuggestions: true },
69+
create(context) {
70+
return {
71+
[`${CallExpression}[expression.callee.name=${/^(Object|Reflect)DefinePropert(ies|y)$/}]`](
72+
node
73+
) {
74+
switch (node.expression.callee.name) {
75+
case 'ObjectDefineProperties':
76+
checkProperties(context, node.expression.arguments[1]);
77+
break;
78+
case 'ReflectDefineProperty':
79+
case 'ObjectDefineProperty':
80+
checkPropertyDescriptor(context, node.expression.arguments[2]);
81+
break;
82+
default:
83+
throw new Error('Unreachable');
84+
}
85+
},
86+
87+
[`${CallExpression}[expression.callee.name="ObjectCreate"][expression.arguments.length=2]`](node) {
88+
checkProperties(context, node.expression.arguments[1]);
89+
},
90+
};
91+
},
92+
};

0 commit comments

Comments
 (0)