test_runner: add `__proto__` null

PR-URL: https://github.com/nodejs/node/pull/48663
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
This commit is contained in:
Raz Luvaton 2023-07-29 16:22:16 +03:00 committed by GitHub
parent 42b5711d0b
commit ee391f3781
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 182 additions and 24 deletions

View File

@ -111,6 +111,14 @@ module.exports = {
},
] },
},
{
files: [
'lib/internal/test_runner/**/*.js',
],
rules: {
'node-core/set-proto-to-null-in-object': 'error',
},
},
],
rules: {
// ESLint built-in rules

View File

@ -250,7 +250,7 @@ class TestCoverage {
let dir;
try {
mkdirSync(this.originalCoverageDirectory, { recursive: true });
mkdirSync(this.originalCoverageDirectory, { __proto__: null, recursive: true });
dir = opendirSync(this.coverageDirectory);
for (let entry; (entry = dir.readSync()) !== null;) {

View File

@ -116,6 +116,7 @@ function setup(root) {
const globalOptions = parseCommandLine();
const hook = createHook({
__proto__: null,
init(asyncId, type, triggerAsyncId, resource) {
if (resource instanceof Test) {
testResources.set(asyncId, resource);
@ -216,7 +217,7 @@ function runInParentContext(Factory) {
const test = (name, options, fn) => run(name, options, fn);
ArrayPrototypeForEach(['skip', 'todo', 'only'], (keyword) => {
test[keyword] = (name, options, fn) => run(name, options, fn, { [keyword]: true });
test[keyword] = (name, options, fn) => run(name, options, fn, { __proto__: null, [keyword]: true });
});
return test;
}

View File

@ -133,7 +133,7 @@ class MockTracker {
validateObject(options, 'options');
const { times = Infinity } = options;
validateTimes(times, 'options.times');
const ctx = new MockFunctionContext(implementation, { original }, times);
const ctx = new MockFunctionContext(implementation, { __proto__: null, original }, times);
return this.#setupMock(ctx, original);
}
@ -189,7 +189,7 @@ class MockTracker {
);
}
const restore = { descriptor, object: objectOrFunction, methodName };
const restore = { __proto__: null, descriptor, object: objectOrFunction, methodName };
const impl = implementation === kDefaultFunction ?
original : implementation;
const ctx = new MockFunctionContext(impl, restore, times);
@ -238,6 +238,7 @@ class MockTracker {
}
return this.method(object, methodName, implementation, {
__proto__: null,
...options,
getter,
});
@ -265,6 +266,7 @@ class MockTracker {
}
return this.method(object, methodName, implementation, {
__proto__: null,
...options,
setter,
});
@ -297,6 +299,7 @@ class MockTracker {
throw err;
} finally {
FunctionPrototypeCall(trackCall, ctx, {
__proto__: null,
arguments: argList,
error,
result,
@ -321,6 +324,7 @@ class MockTracker {
throw err;
} finally {
FunctionPrototypeCall(trackCall, ctx, {
__proto__: null,
arguments: argList,
error,
result,

View File

@ -45,7 +45,7 @@ function setPosition(node, pos) {
}
function abortIt(signal) {
return new AbortError(undefined, { cause: signal.reason });
return new AbortError(undefined, { __proto__: null, cause: signal.reason });
}
const SUPPORTED_TIMERS = ['setTimeout', 'setInterval'];
@ -194,7 +194,9 @@ class MockTimers {
#toggleEnableTimers(activate) {
const options = {
__proto__: null,
toFake: {
__proto__: null,
setTimeout: () => {
this.#realSetTimeout = globalThis.setTimeout;
this.#realClearTimeout = globalThis.clearTimeout;
@ -233,6 +235,7 @@ class MockTimers {
},
},
toReal: {
__proto__: null,
setTimeout: () => {
globalThis.setTimeout = this.#realSetTimeout;
globalThis.clearTimeout = this.#realClearTimeout;

View File

@ -42,7 +42,7 @@ class SpecReporter extends Transform {
#failedTests = [];
constructor() {
super({ writableObjectMode: true });
super({ __proto__: null, writableObjectMode: true });
}
#indent(nesting) {
@ -127,7 +127,7 @@ class SpecReporter extends Transform {
}
}
_transform({ type, data }, encoding, callback) {
callback(null, this.#handleEvent({ type, data }));
callback(null, this.#handleEvent({ __proto__: null, type, data }));
}
_flush(callback) {
if (this.#failedTests.length === 0) {

View File

@ -18,7 +18,7 @@ const kDefaultIndent = ' '; // 4 spaces
const kFrameStartRegExp = /^ {4}at /;
const kLineBreakRegExp = /\n|\r\n/;
const kDefaultTAPVersion = 13;
const inspectOptions = { colors: false, breakLength: Infinity };
const inspectOptions = { __proto__: null, colors: false, breakLength: Infinity };
let testModule; // Lazy loaded due to circular dependency.
function lazyLoadTest() {

View File

@ -304,9 +304,9 @@ class FileTest extends Test {
function runTestFile(path, root, inspectPort, filesWatcher, testNamePatterns) {
const watchMode = filesWatcher != null;
const subtest = root.createSubtest(FileTest, path, async (t) => {
const args = getRunArgs({ path, inspectPort, testNamePatterns });
const args = getRunArgs({ __proto__: null, path, inspectPort, testNamePatterns });
const stdio = ['pipe', 'pipe', 'pipe'];
const env = { ...process.env, NODE_TEST_CONTEXT: 'child-v8' };
const env = { __proto__: null, ...process.env, NODE_TEST_CONTEXT: 'child-v8' };
if (watchMode) {
stdio.push('ipc');
env.WATCH_REPORT_DEPENDENCIES = '1';
@ -315,7 +315,7 @@ function runTestFile(path, root, inspectPort, filesWatcher, testNamePatterns) {
env.FORCE_COLOR = '1';
}
const child = spawn(process.execPath, args, { signal: t.signal, encoding: 'utf8', env, stdio });
const child = spawn(process.execPath, args, { __proto__: null, signal: t.signal, encoding: 'utf8', env, stdio });
if (watchMode) {
filesWatcher.runningProcesses.set(path, child);
filesWatcher.watcher.watchChildProcessModules(child, path);
@ -332,7 +332,7 @@ function runTestFile(path, root, inspectPort, filesWatcher, testNamePatterns) {
subtest.parseMessage(data);
});
const rl = createInterface({ input: child.stderr });
const rl = createInterface({ __proto__: null, input: child.stderr });
rl.on('line', (line) => {
if (isInspectorMessage(line)) {
process.stderr.write(line + '\n');
@ -350,8 +350,8 @@ function runTestFile(path, root, inspectPort, filesWatcher, testNamePatterns) {
});
const { 0: { 0: code, 1: signal } } = await SafePromiseAll([
once(child, 'exit', { signal: t.signal }),
finished(child.stdout, { signal: t.signal }),
once(child, 'exit', { __proto__: null, signal: t.signal }),
finished(child.stdout, { __proto__: null, signal: t.signal }),
]);
if (watchMode) {
@ -384,7 +384,7 @@ function runTestFile(path, root, inspectPort, filesWatcher, testNamePatterns) {
function watchFiles(testFiles, root, inspectPort, signal, testNamePatterns) {
const runningProcesses = new SafeMap();
const runningSubtests = new SafeMap();
const watcher = new FilesWatcher({ debounce: 200, mode: 'filter', signal });
const watcher = new FilesWatcher({ __proto__: null, debounce: 200, mode: 'filter', signal });
const filesWatcher = { __proto__: null, watcher, runningProcesses, runningSubtests };
watcher.on('changed', ({ owners }) => {
@ -469,7 +469,7 @@ function run(options) {
});
}
const root = createTestTree({ concurrency, timeout, signal });
const root = createTestTree({ __proto__: null, concurrency, timeout, signal });
let testFiles = files ?? createTestFileList();
if (shard) {

View File

@ -79,7 +79,7 @@ function stopTest(timeout, signal) {
if (timeout === kDefaultTimeout) {
return once(signal, 'abort');
}
return PromisePrototypeThen(setTimeout(timeout, null, { ref: false, signal }), () => {
return PromisePrototypeThen(setTimeout(timeout, null, { __proto__: null, ref: false, signal }), () => {
throw new ERR_TEST_FAILURE(
`test timed out after ${timeout}ms`,
kTestTimeoutFailure,
@ -506,7 +506,7 @@ class Test extends AsyncResource {
getRunArgs() {
const ctx = new TestContext(this);
return { ctx, args: [ctx] };
return { __proto__: null, ctx, args: [ctx] };
}
async runHook(hook, args) {
@ -540,12 +540,12 @@ class Test extends AsyncResource {
const { args, ctx } = this.getRunArgs();
const after = async () => {
if (this.hooks.after.length > 0) {
await this.runHook('after', { args, ctx });
await this.runHook('after', { __proto__: null, args, ctx });
}
};
const afterEach = runOnce(async () => {
if (this.parent?.hooks.afterEach.length > 0) {
await this.parent.runHook('afterEach', { args, ctx });
await this.parent.runHook('afterEach', { __proto__: null, args, ctx });
}
});
@ -554,7 +554,7 @@ class Test extends AsyncResource {
await this.parent.runHook('before', this.parent.getRunArgs());
}
if (this.parent?.hooks.beforeEach.length > 0) {
await this.parent.runHook('beforeEach', { args, ctx });
await this.parent.runHook('beforeEach', { __proto__: null, args, ctx });
}
const stopPromise = stopTest(this.timeout, this.signal);
const runArgs = ArrayPrototypeSlice(args);
@ -811,7 +811,7 @@ class Suite extends Test {
getRunArgs() {
const ctx = new SuiteContext(this);
return { ctx, args: [ctx] };
return { __proto__: null, ctx, args: [ctx] };
}
async run() {

View File

@ -12,7 +12,7 @@ class TestsStream extends Readable {
#canPush;
constructor() {
super({ objectMode: true });
super({ __proto__: null, objectMode: true });
this.#buffer = [];
this.#canPush = true;
}
@ -83,6 +83,8 @@ class TestsStream extends Readable {
[kEmitMessage](type, data) {
this.emit(type, data);
// Disabling as this going to the user-land
// eslint-disable-next-line node-core/set-proto-to-null-in-object
this.#tryPush({ type, data });
}

View File

@ -75,7 +75,7 @@ function createDeferredCallback() {
resolve();
};
return { promise, cb };
return { __proto__: null, promise, cb };
}
function isTestFailureError(err) {

View File

@ -0,0 +1,140 @@
'use strict';
module.exports = {
meta: {
messages: {
error: 'Add `__proto__: null` to object',
},
fixable: 'code',
},
create: function(context) {
return {
ObjectExpression(node) {
// Not adding __proto__ to module.exports as it will break a lot of libraries
if (isModuleExportsObject(node) || isModifiedExports(node)) {
return;
}
const properties = node.properties;
let hasProto = false;
for (const property of properties) {
if (!property.key) {
continue;
}
if (property.key.type === 'Identifier' && property.key.name === '__proto__') {
hasProto = true;
break;
}
if (property.key.type === 'Literal' && property.key.value === '__proto__') {
hasProto = true;
break;
}
}
if (hasProto) {
return;
}
if (properties.length > 0) {
// If the object has properties but no __proto__ property
context.report({
node,
message: 'Every object must have __proto__: null',
fix: function(fixer) {
// Generate the fix suggestion to add __proto__: null
const sourceCode = context.getSourceCode();
const firstProperty = properties[0];
const firstPropertyToken = sourceCode.getFirstToken(firstProperty);
const isMultiLine = properties.length === 1 ?
// If the object has only one property,
// it's multiline if the property is not on the same line as the object parenthesis
properties[0].loc.start.line !== node.loc.start.line :
// If the object has more than one property,
// it's multiline if the first and second properties are not on the same line
properties[0].loc.start.line !== properties[1].loc.start.line;
const fixText = `__proto__: null,${isMultiLine ? '\n' : ' '}`;
// Insert the fix suggestion before the first property
return fixer.insertTextBefore(firstPropertyToken, fixText);
},
});
}
if (properties.length === 0) {
// If the object is empty and missing __proto__
context.report({
node,
message: 'Every empty object must have __proto__: null',
fix: function(fixer) {
// Generate the fix suggestion to create the object with __proto__: null
const fixText = '{ __proto__: null }';
// Replace the empty object with the fix suggestion
return fixer.replaceText(node, fixText);
},
});
}
},
};
},
};
// Helper function to check if the object is `module.exports`
function isModuleExportsObject(node) {
return (
node.parent &&
node.parent.type === 'AssignmentExpression' &&
node.parent.left &&
node.parent.left.type === 'MemberExpression' &&
node.parent.left.object &&
node.parent.left.object.name === 'module' &&
node.parent.left.property &&
node.parent.left.property.name === 'exports'
);
}
function isModifiedExports(node) {
return (
node.parent &&
(isObjectAssignCall(node.parent) || isObjectDefinePropertiesCall(node.parent))
);
}
// Helper function to check if the node represents an ObjectAssign call
function isObjectAssignCall(node) {
return (
node.type === 'CallExpression' &&
node.callee &&
node.callee.type === 'Identifier' &&
node.callee.name === 'ObjectAssign' &&
node.arguments.length > 1 &&
node.arguments.some((arg) =>
arg.type === 'MemberExpression' &&
arg.object.name === 'module' &&
arg.property.name === 'exports',
)
);
}
// Helper function to check if the node represents an ObjectDefineProperties call
function isObjectDefinePropertiesCall(node) {
return (
node.type === 'CallExpression' &&
node.callee &&
node.callee.type === 'Identifier' &&
node.callee.name === 'ObjectDefineProperties' &&
node.arguments.length > 1 &&
node.arguments.some((arg) =>
arg.type === 'MemberExpression' &&
arg.object.name === 'module' &&
arg.property.name === 'exports',
)
);
}