node/tools/eslint-rules/avoid-prototype-pollution.js
Michaël Zasso 7e6d92c485
tools: update ESLint to v9 and use flat config
Closes: https://github.com/nodejs/node/issues/52567
PR-URL: https://github.com/nodejs/node/pull/52780
Fixes: https://github.com/nodejs/node/issues/52567
Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
Reviewed-By: Yagiz Nizipli <yagiz.nizipli@sentry.io>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
2024-05-23 19:45:18 +00:00

238 lines
8.2 KiB
JavaScript

'use strict';
const CallExpression = (fnName) => `CallExpression[callee.name=${fnName}]`;
const AnyFunction = 'FunctionDeclaration, FunctionExpression, ArrowFunctionExpression';
function checkProperties(context, node) {
if (
node.type === 'CallExpression' &&
node.callee.name === 'ObjectGetOwnPropertyDescriptors'
) {
context.report({
node,
message:
'Property descriptors inherits from the Object prototype, therefore are subject to prototype pollution',
});
}
if (node.type !== 'ObjectExpression') return;
for (const { key, value } of node.properties) {
if (
key != null && value != null &&
!(key.type === 'Identifier' && key.name === '__proto__') &&
!(key.type === 'Literal' && key.value === '__proto__')
) {
checkPropertyDescriptor(context, value);
}
}
}
function isNullPrototypeObjectExpression(node) {
if (node.type !== 'ObjectExpression') return;
for (const { key, value } of node.properties) {
if (
key != null && value != null &&
((key.type === 'Identifier' && key.name === '__proto__') ||
(key.type === 'Literal' && key.value === '__proto__')) &&
value.type === 'Literal' && value.value === null
) {
return true;
}
}
return false;
}
function checkPropertyDescriptor(context, node) {
if (
node.type === 'CallExpression' &&
(node.callee.name === 'ObjectGetOwnPropertyDescriptor' ||
node.callee.name === 'ReflectGetOwnPropertyDescriptor')
) {
context.report({
node,
message:
'Property descriptors inherits from the Object prototype, therefore are subject to prototype pollution',
suggest: [{
desc: 'Wrap the property descriptor in a null-prototype object',
fix(fixer) {
return [
fixer.insertTextBefore(node, '{ __proto__: null,...'),
fixer.insertTextAfter(node, ' }'),
];
},
}],
});
}
if (isNullPrototypeObjectExpression(node) === false) {
context.report({
node,
message: 'Must use null-prototype object for property descriptors',
});
}
}
function createUnsafeStringMethodReport(context, name, lookedUpProperty) {
const lastDotPosition = '$String.prototype.'.length;
const unsafePrimordialName = `StringPrototype${name.charAt(lastDotPosition).toUpperCase()}${name.slice(lastDotPosition + 1, -1)}`;
return {
[CallExpression(unsafePrimordialName)](node) {
context.report({
node,
message: `${name} looks up the ${lookedUpProperty} property on the first argument`,
});
},
};
}
function createUnsafeStringMethodOnRegexReport(context, name, lookedUpProperty) {
const dotPosition = 'Symbol.'.length;
const safePrimordialName = `RegExpPrototypeSymbol${lookedUpProperty.charAt(dotPosition).toUpperCase()}${lookedUpProperty.slice(dotPosition + 1)}`;
const lastDotPosition = '$String.prototype.'.length;
const unsafePrimordialName = `StringPrototype${name.charAt(lastDotPosition).toUpperCase()}${name.slice(lastDotPosition + 1, -1)}`;
return {
[[
`${CallExpression(unsafePrimordialName)}[arguments.1.type=Literal][arguments.1.regex]`,
`${CallExpression(unsafePrimordialName)}[arguments.1.type=NewExpression][arguments.1.callee.name=RegExp]`,
].join(',')](node) {
context.report({
node,
message: `${name} looks up the ${lookedUpProperty} property of the passed regex, use ${safePrimordialName} directly`,
});
},
};
}
module.exports = {
meta: { hasSuggestions: true },
create(context) {
return {
[CallExpression(/^(Object|Reflect)DefinePropert(ies|y)$/)](node) {
switch (node.callee.name) {
case 'ObjectDefineProperties':
checkProperties(context, node.arguments[1]);
break;
case 'ReflectDefineProperty':
case 'ObjectDefineProperty':
checkPropertyDescriptor(context, node.arguments[2]);
break;
default:
throw new Error('Unreachable');
}
},
[`${CallExpression('ObjectCreate')}[arguments.length=2]`](node) {
checkProperties(context, node.arguments[1]);
},
[`:matches(${AnyFunction})[async=true]>BlockStatement ReturnStatement>ObjectExpression, :matches(${AnyFunction})[async=true]>ObjectExpression`](node) {
if (node.parent.type === 'ReturnStatement') {
let { parent } = node;
do {
({ parent } = parent);
} while (!parent.type.includes('Function'));
if (!parent.async) return;
}
if (isNullPrototypeObjectExpression(node) === false) {
context.report({
node,
message: 'Use null-prototype when returning from async function',
});
}
},
[CallExpression('RegExpPrototypeTest')](node) {
context.report({
node,
message: '%RegExp.prototype.test% looks up the "exec" property of `this` value',
suggest: [{
desc: 'Use RegexpPrototypeExec instead',
fix(fixer) {
const testRange = [ ...node.range ];
testRange[0] = testRange[0] + 'RegexpPrototype'.length;
testRange[1] = testRange[0] + 'Test'.length;
return [
fixer.replaceTextRange(testRange, 'Exec'),
fixer.insertTextAfter(node, ' !== null'),
];
},
}],
});
},
[CallExpression(/^RegExpPrototypeSymbol(Match|MatchAll)$/)](node) {
context.report({
node,
message: node.callee.name + ' looks up the "exec" property of `this` value',
});
},
[CallExpression(/^(RegExpPrototypeSymbol|StringPrototype)Search$/)](node) {
context.report({
node,
message: node.callee.name + ' is unsafe, use SafeStringPrototypeSearch instead',
});
},
...createUnsafeStringMethodReport(context, '%String.prototype.match%', 'Symbol.match'),
...createUnsafeStringMethodReport(context, '%String.prototype.matchAll%', 'Symbol.matchAll'),
...createUnsafeStringMethodOnRegexReport(context, '%String.prototype.replace%', 'Symbol.replace'),
...createUnsafeStringMethodOnRegexReport(context, '%String.prototype.replaceAll%', 'Symbol.replace'),
...createUnsafeStringMethodOnRegexReport(context, '%String.prototype.split%', 'Symbol.split'),
'NewExpression[callee.name="Proxy"][arguments.1.type="ObjectExpression"]'(node) {
for (const { key, value } of node.arguments[1].properties) {
if (
key != null && value != null &&
((key.type === 'Identifier' && key.name === '__proto__') ||
(key.type === 'Literal' && key.value === '__proto__')) &&
value.type === 'Literal' && value.value === null
) {
return;
}
}
context.report({
node,
message: 'Proxy handler must be a null-prototype object',
});
},
[`ExpressionStatement>AwaitExpression>${CallExpression(/^(Safe)?PromiseAll(Settled)?$/)}`](node) {
context.report({
node,
message: `Use ${node.callee.name}ReturnVoid`,
});
},
[CallExpression('PromisePrototypeCatch')](node) {
context.report({
node,
message: '%Promise.prototype.catch% looks up the `then` property of ' +
'the `this` argument, use PromisePrototypeThen instead',
});
},
[CallExpression('PromisePrototypeFinally')](node) {
context.report({
node,
message: '%Promise.prototype.finally% looks up the `then` property of ' +
'the `this` argument, use SafePromisePrototypeFinally or ' +
'try/finally instead',
});
},
[CallExpression(/^Promise(All(Settled)?|Any|Race)/)](node) {
context.report({
node,
message: `Use Safe${node.callee.name} instead of ${node.callee.name}`,
});
},
[CallExpression('ArrayPrototypeConcat')](node) {
context.report({
node,
message: '%Array.prototype.concat% looks up `@@isConcatSpreadable` ' +
'which can be subject to prototype pollution',
});
},
};
},
};