Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit c7b7f2b

Browse filesBrowse files
aduh95RafaelGSS
authored andcommitted
lib: add lint rule to protect against Object.prototype.then pollution
PR-URL: #45061 Reviewed-By: Jacob Smith <jacob@frende.me> Reviewed-By: Moshe Atlow <moshe@atlow.co.il> Reviewed-By: Minwoo Jung <nodecorelab@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
1 parent 93a34fa commit c7b7f2b
Copy full SHA for c7b7f2b

File tree

Expand file treeCollapse file tree

10 files changed

+98
-33
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

10 files changed

+98
-33
lines changed
Open diff view settings
Collapse file

‎lib/internal/crypto/cfrg.js‎

Copy file name to clipboardExpand all lines: lib/internal/crypto/cfrg.js
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ async function cfrgGenerateKey(algorithm, extractable, keyUsages) {
188188
privateUsages,
189189
extractable);
190190

191-
return { privateKey, publicKey };
191+
return { __proto__: null, privateKey, publicKey };
192192
}
193193

194194
function cfrgExportKey(key, format) {
Collapse file

‎lib/internal/crypto/ec.js‎

Copy file name to clipboardExpand all lines: lib/internal/crypto/ec.js
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ async function ecGenerateKey(algorithm, extractable, keyUsages) {
146146
privateUsages,
147147
extractable);
148148

149-
return { publicKey, privateKey };
149+
return { __proto__: null, publicKey, privateKey };
150150
}
151151

152152
function ecExportKey(key, format) {
Collapse file

‎lib/internal/crypto/rsa.js‎

Copy file name to clipboardExpand all lines: lib/internal/crypto/rsa.js
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ async function rsaKeyGenerate(
219219
privateUsages,
220220
extractable);
221221

222-
return { publicKey, privateKey };
222+
return { __proto__: null, publicKey, privateKey };
223223
}
224224

225225
function rsaExportKey(key, format) {
Collapse file

‎lib/internal/fs/cp/cp.js‎

Copy file name to clipboardExpand all lines: lib/internal/fs/cp/cp.js
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ async function checkPaths(src, dest, opts) {
115115
code: 'EINVAL',
116116
});
117117
}
118-
return { srcStat, destStat, skipped: false };
118+
return { __proto__: null, srcStat, destStat, skipped: false };
119119
}
120120

121121
function areIdentical(srcStat, destStat) {
Collapse file

‎lib/internal/fs/promises.js‎

Copy file name to clipboardExpand all lines: lib/internal/fs/promises.js
+8-8Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,7 @@ async function read(handle, bufferOrParams, offset, length, position) {
580580
length |= 0;
581581

582582
if (length === 0)
583-
return { bytesRead: length, buffer };
583+
return { __proto__: null, bytesRead: length, buffer };
584584

585585
if (buffer.byteLength === 0) {
586586
throw new ERR_INVALID_ARG_VALUE('buffer', buffer,
@@ -595,7 +595,7 @@ async function read(handle, bufferOrParams, offset, length, position) {
595595
const bytesRead = (await binding.read(handle.fd, buffer, offset, length,
596596
position, kUsePromises)) || 0;
597597

598-
return { bytesRead, buffer };
598+
return { __proto__: null, bytesRead, buffer };
599599
}
600600

601601
async function readv(handle, buffers, position) {
@@ -606,12 +606,12 @@ async function readv(handle, buffers, position) {
606606

607607
const bytesRead = (await binding.readBuffers(handle.fd, buffers, position,
608608
kUsePromises)) || 0;
609-
return { bytesRead, buffers };
609+
return { __proto__: null, bytesRead, buffers };
610610
}
611611

612612
async function write(handle, buffer, offsetOrOptions, length, position) {
613613
if (buffer?.byteLength === 0)
614-
return { bytesWritten: 0, buffer };
614+
return { __proto__: null, bytesWritten: 0, buffer };
615615

616616
let offset = offsetOrOptions;
617617
if (isArrayBufferView(buffer)) {
@@ -636,14 +636,14 @@ async function write(handle, buffer, offsetOrOptions, length, position) {
636636
const bytesWritten =
637637
(await binding.writeBuffer(handle.fd, buffer, offset,
638638
length, position, kUsePromises)) || 0;
639-
return { bytesWritten, buffer };
639+
return { __proto__: null, bytesWritten, buffer };
640640
}
641641

642642
validateStringAfterArrayBufferView(buffer, 'buffer');
643643
validateEncoding(buffer, length);
644644
const bytesWritten = (await binding.writeString(handle.fd, buffer, offset,
645645
length, kUsePromises)) || 0;
646-
return { bytesWritten, buffer };
646+
return { __proto__: null, bytesWritten, buffer };
647647
}
648648

649649
async function writev(handle, buffers, position) {
@@ -653,12 +653,12 @@ async function writev(handle, buffers, position) {
653653
position = null;
654654

655655
if (buffers.length === 0) {
656-
return { bytesWritten: 0, buffers };
656+
return { __proto__: null, bytesWritten: 0, buffers };
657657
}
658658

659659
const bytesWritten = (await binding.writeBuffers(handle.fd, buffers, position,
660660
kUsePromises)) || 0;
661-
return { bytesWritten, buffers };
661+
return { __proto__: null, bytesWritten, buffers };
662662
}
663663

664664
async function rename(oldPath, newPath) {
Collapse file

‎lib/internal/modules/esm/loader.js‎

Copy file name to clipboardExpand all lines: lib/internal/modules/esm/loader.js
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,7 @@ class ESMLoader {
378378
const { module } = await job.run();
379379

380380
return {
381+
__proto__: null,
381382
namespace: module.getNamespace(),
382383
};
383384
}
Collapse file

‎lib/internal/modules/esm/resolve.js‎

Copy file name to clipboardExpand all lines: lib/internal/modules/esm/resolve.js
+3-2Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -977,7 +977,7 @@ async function defaultResolve(specifier, context = {}) {
977977
missing = false;
978978
} else if (destination) {
979979
const href = destination.href;
980-
return { url: href };
980+
return { __proto__: null, url: href };
981981
}
982982
if (missing) {
983983
// Prevent network requests from firing if resolution would be banned.
@@ -1035,7 +1035,7 @@ async function defaultResolve(specifier, context = {}) {
10351035
if (maybeReturn) return maybeReturn;
10361036

10371037
// This must come after checkIfDisallowedImport
1038-
if (parsed && parsed.protocol === 'node:') return { url: specifier };
1038+
if (parsed && parsed.protocol === 'node:') return { __proto__: null, url: specifier };
10391039

10401040
throwIfUnsupportedURLScheme(parsed, experimentalNetworkImports);
10411041

@@ -1087,6 +1087,7 @@ async function defaultResolve(specifier, context = {}) {
10871087
throwIfUnsupportedURLProtocol(url);
10881088

10891089
return {
1090+
__proto__: null,
10901091
// Do NOT cast `url` to a string: that will work even when there are real
10911092
// problems, silencing them
10921093
url: url.href,
Collapse file

‎lib/internal/webstreams/readablestream.js‎

Copy file name to clipboardExpand all lines: lib/internal/webstreams/readablestream.js
+3-3Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,7 @@ class ReadableStream {
476476

477477
async function returnSteps(value) {
478478
if (done)
479-
return { done: true, value };
479+
return { done: true, value }; // eslint-disable-line node-core/avoid-prototype-pollution
480480
done = true;
481481

482482
if (reader[kState].stream === undefined) {
@@ -488,11 +488,11 @@ class ReadableStream {
488488
const result = readableStreamReaderGenericCancel(reader, value);
489489
readableStreamReaderGenericRelease(reader);
490490
await result;
491-
return { done: true, value };
491+
return { done: true, value }; // eslint-disable-line node-core/avoid-prototype-pollution
492492
}
493493

494494
readableStreamReaderGenericRelease(reader);
495-
return { done: true, value };
495+
return { done: true, value }; // eslint-disable-line node-core/avoid-prototype-pollution
496496
}
497497

498498
// TODO(@jasnell): Explore whether an async generator
Collapse file

‎test/parallel/test-eslint-avoid-prototype-pollution.js‎

Copy file name to clipboardExpand all lines: test/parallel/test-eslint-avoid-prototype-pollution.js
+39Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,17 @@ new RuleTester({
4545
'ReflectDefineProperty({}, "key", { "__proto__": null })',
4646
'ObjectDefineProperty({}, "key", { \'__proto__\': null })',
4747
'ReflectDefineProperty({}, "key", { \'__proto__\': null })',
48+
'async function myFn() { return { __proto__: null } }',
49+
'async function myFn() { function myFn() { return {} } return { __proto__: null } }',
50+
'const myFn = async function myFn() { return { __proto__: null } }',
51+
'const myFn = async function () { return { __proto__: null } }',
52+
'const myFn = async () => { return { __proto__: null } }',
53+
'const myFn = async () => ({ __proto__: null })',
54+
'function myFn() { return {} }',
55+
'const myFn = function myFn() { return {} }',
56+
'const myFn = function () { return {} }',
57+
'const myFn = () => { return {} }',
58+
'const myFn = () => ({})',
4859
'StringPrototypeReplace("some string", "some string", "some replacement")',
4960
'StringPrototypeReplaceAll("some string", "some string", "some replacement")',
5061
'StringPrototypeSplit("some string", "some string")',
@@ -150,6 +161,34 @@ new RuleTester({
150161
code: 'ReflectDefineProperty({}, "key", { enumerable: true })',
151162
errors: [{ message: /null-prototype/ }],
152163
},
164+
{
165+
code: 'async function myFn(){ return {} }',
166+
errors: [{ message: /null-prototype/ }],
167+
},
168+
{
169+
code: 'async function myFn(){ async function someOtherFn() { return { __proto__: null } } return {} }',
170+
errors: [{ message: /null-prototype/ }],
171+
},
172+
{
173+
code: 'async function myFn(){ if (true) { return {} } return { __proto__: null } }',
174+
errors: [{ message: /null-prototype/ }],
175+
},
176+
{
177+
code: 'const myFn = async function myFn(){ return {} }',
178+
errors: [{ message: /null-prototype/ }],
179+
},
180+
{
181+
code: 'const myFn = async function (){ return {} }',
182+
errors: [{ message: /null-prototype/ }],
183+
},
184+
{
185+
code: 'const myFn = async () => { return {} }',
186+
errors: [{ message: /null-prototype/ }],
187+
},
188+
{
189+
code: 'const myFn = async () => ({})',
190+
errors: [{ message: /null-prototype/ }],
191+
},
153192
{
154193
code: 'RegExpPrototypeTest(/some regex/, "some string")',
155194
errors: [{ message: /looks up the "exec" property/ }],
Collapse file

‎tools/eslint-rules/avoid-prototype-pollution.js‎

Copy file name to clipboardExpand all lines: tools/eslint-rules/avoid-prototype-pollution.js
+40-16Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22

33
const CallExpression = (fnName) => `CallExpression[callee.name=${fnName}]`;
4+
const AnyFunction = 'FunctionDeclaration, FunctionExpression, ArrowFunctionExpression';
45

56
function checkProperties(context, node) {
67
if (
@@ -25,6 +26,22 @@ function checkProperties(context, node) {
2526
}
2627
}
2728

29+
function isNullPrototypeObjectExpression(node) {
30+
if (node.type !== 'ObjectExpression') return;
31+
32+
for (const { key, value } of node.properties) {
33+
if (
34+
key != null && value != null &&
35+
((key.type === 'Identifier' && key.name === '__proto__') ||
36+
(key.type === 'Literal' && key.value === '__proto__')) &&
37+
value.type === 'Literal' && value.value === null
38+
) {
39+
return true;
40+
}
41+
}
42+
return false;
43+
}
44+
2845
function checkPropertyDescriptor(context, node) {
2946
if (
3047
node.type === 'CallExpression' &&
@@ -46,23 +63,12 @@ function checkPropertyDescriptor(context, node) {
4663
}],
4764
});
4865
}
49-
if (node.type !== 'ObjectExpression') return;
50-
51-
for (const { key, value } of node.properties) {
52-
if (
53-
key != null && value != null &&
54-
((key.type === 'Identifier' && key.name === '__proto__') ||
55-
(key.type === 'Literal' && key.value === '__proto__')) &&
56-
value.type === 'Literal' && value.value === null
57-
) {
58-
return true;
59-
}
66+
if (isNullPrototypeObjectExpression(node) === false) {
67+
context.report({
68+
node,
69+
message: 'Must use null-prototype object for property descriptors',
70+
});
6071
}
61-
62-
context.report({
63-
node,
64-
message: 'Must use null-prototype object for property descriptors',
65-
});
6672
}
6773

6874
function createUnsafeStringMethodReport(context, name, lookedUpProperty) {
@@ -117,6 +123,24 @@ module.exports = {
117123
[`${CallExpression('ObjectCreate')}[arguments.length=2]`](node) {
118124
checkProperties(context, node.arguments[1]);
119125
},
126+
127+
[`:matches(${AnyFunction})[async=true]>BlockStatement ReturnStatement>ObjectExpression, :matches(${AnyFunction})[async=true]>ObjectExpression`](node) {
128+
if (node.parent.type === 'ReturnStatement') {
129+
let { parent } = node;
130+
do {
131+
({ parent } = parent);
132+
} while (!parent.type.includes('Function'));
133+
134+
if (!parent.async) return;
135+
}
136+
if (isNullPrototypeObjectExpression(node) === false) {
137+
context.report({
138+
node,
139+
message: 'Use null-prototype when returning from async function',
140+
});
141+
}
142+
},
143+
120144
[CallExpression('RegExpPrototypeTest')](node) {
121145
context.report({
122146
node,

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.