@locker/eslint-plugin-unsafe-types
Version:
Detect usage of unsigned unsafe types.
306 lines (269 loc) • 11.4 kB
JavaScript
/** Original file https://github.com/eslint/eslint/blob/main/lib/rules/no-eval.js
* Code specifically written by LWS team is marked between // LWS BEGIN and // LWS END comments
* @fileoverview Rule to flag use of eval() statement
* @author Lightning Web Security Team
*/
'use strict';
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require('../utils/ast-utils');
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const candidatesOfGlobalObject = Object.freeze(['global', 'window', 'globalThis']);
/**
* Checks a given node is a MemberExpression node which has the specified name's
* property.
* @param {ASTNode} node A node to check.
* @param {string} name A name to check.
* @returns {boolean} `true` if the node is a MemberExpression node which has
* the specified name's property
*/
function isMember(node, name) {
return astUtils.isSpecificMemberAccess(node, null, name);
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../shared/types').Rule} */
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'Disallow the use of unsigned `eval()`',
recommended: true,
url: './docs/rules/no-eval',
},
messages: {
unexpected: 'eval can be harmful.',
},
},
create(context) {
const sourceCode = context.sourceCode || context.getSourceCode();
const getScope = function (node) {
if (typeof sourceCode.getScope === 'function') {
return sourceCode.getScope(node);
}
if (typeof context.getScope === 'function') {
return context.getScope();
}
throw new Error('Cannot get scope of current node!');
};
let funcInfo = null;
const callees = new Map();
/**
* Pushes a `this` scope (non-arrow function, class static block, or class field initializer) information to the stack.
* Top-level scopes are handled separately.
*
* This is used in order to check whether or not `this` binding is a
* reference to the global object.
* @param {ASTNode} node A node of the scope.
* For functions, this is one of FunctionDeclaration, FunctionExpression.
* For class static blocks, this is StaticBlock.
* For class field initializers, this can be any node that is PropertyDefinition#value.
* @returns {void}
*/
function enterThisScope(node) {
const strict = getScope(node).isStrict;
funcInfo = {
upper: funcInfo,
node,
strict,
isTopLevelOfScript: false,
defaultThis: false,
initialized: strict,
};
}
/**
* Pops a variable scope from the stack.
* @returns {void}
*/
function exitThisScope() {
funcInfo = funcInfo.upper;
}
/**
* Reports a given node.
*
* `node` is `Identifier` or `MemberExpression`.
* The parent of `node` might be `CallExpression`.
*
* The location of the report is always `eval` `Identifier` (or possibly
* `Literal`). The type of the report is `CallExpression` if the parent is
* `CallExpression`. Otherwise, it's the given node type.
* @param {ASTNode} node A node to report.
* @returns {void}
*/
function report(node) {
const parent = node.parent;
const locationNode = node.type === 'MemberExpression' ? node.property : node;
const reportNode =
parent.type === 'CallExpression' && parent.callee === node ? parent : node;
context.report({
node: reportNode,
loc: locationNode.loc,
messageId: 'unexpected',
});
}
/**
* Reports accesses of `eval` via the global object.
* @param {eslint-scope.Scope} globalScope The global scope.
* @returns {void}
*/
function reportAccessingEvalViaGlobalObject(globalScope) {
for (let i = 0; i < candidatesOfGlobalObject.length; ++i) {
const name = candidatesOfGlobalObject[i];
const variable = astUtils.getVariableByName(globalScope, name);
if (!variable) {
continue;
}
const references = variable.references;
for (let j = 0; j < references.length; ++j) {
const identifier = references[j].identifier;
let node = identifier.parent;
// To detect code like `window.window.eval`.
while (isMember(node, name)) {
node = node.parent;
}
// Reports.
if (isMember(node, 'eval')) {
// LWS BEGIN
let argument;
if (node.parent && node.parent.type === 'SequenceExpression') {
argument = node.parent.parent.arguments[0];
} else if (node.parent && node.parent.type === 'CallExpression') {
argument = node.parent.arguments[0];
}
if (argument && astUtils.isSignatureCall(argument)) {
return;
}
// LWS END
report(node);
}
}
}
}
/**
* Reports all accesses of `eval` (excludes direct calls to eval).
* @param {eslint-scope.Scope} globalScope The global scope.
* @returns {void}
*/
function reportAccessingEval(globalScope) {
const variable = astUtils.getVariableByName(globalScope, 'eval');
if (!variable) {
return;
}
const references = variable.references;
for (let i = 0; i < references.length; ++i) {
const reference = references[i];
const id = reference.identifier;
if (id.name === 'eval' && !astUtils.isCallee(id)) {
// LWS BEGIN
// Is accessing to eval (excludes direct calls to eval)
switch (id.parent.type) {
case 'SequenceExpression': {
const argument = id.parent.parent.arguments[0];
if (astUtils.isSignatureCall(argument)) {
continue;
} else {
report(id);
break;
}
}
case 'VariableDeclarator': {
const ref = id.parent.id;
const calls = callees.get(ref.name);
calls.forEach((node) => {
const callExpr = node.parent;
const argument = callExpr.arguments[0];
if (argument && !astUtils.isSignatureCall(argument)) {
report(node);
}
});
break;
}
default:
report(id);
}
// LWS END
}
}
}
return {
'CallExpression:exit'(node) {
const callee = node.callee;
// LWS BEGIN
if (callee.type === 'Identifier') {
let nodes = callees.get(callee.name);
if (nodes === undefined) {
nodes = new Set();
}
nodes.add(callee);
callees.set(callee.name, nodes);
} else {
callees.set(callee, callee);
}
const [argument] = node.arguments;
if (astUtils.isSpecificId(callee, 'eval') && !astUtils.isSignatureCall(argument)) {
report(callee);
}
// LWS END
},
Program(node) {
const scope = getScope(node),
strict = scope.isStrict || node.sourceType === 'module',
isTopLevelOfScript = node.sourceType !== 'module';
funcInfo = {
upper: null,
node,
strict,
isTopLevelOfScript,
defaultThis: true,
initialized: true,
};
},
'Program:exit'(node) {
const globalScope = getScope(node);
exitThisScope();
reportAccessingEval(globalScope);
reportAccessingEvalViaGlobalObject(globalScope);
},
FunctionDeclaration: enterThisScope,
'FunctionDeclaration:exit': exitThisScope,
FunctionExpression: enterThisScope,
'FunctionExpression:exit': exitThisScope,
'PropertyDefinition > *.value': enterThisScope,
'PropertyDefinition > *.value:exit': exitThisScope,
StaticBlock: enterThisScope,
'StaticBlock:exit': exitThisScope,
ThisExpression(node) {
if (!isMember(node.parent, 'eval')) {
return;
}
/*
* `this.eval` is found.
* Checks whether or not the value of `this` is the global object.
*/
if (!funcInfo.initialized) {
funcInfo.initialized = true;
funcInfo.defaultThis = astUtils.isDefaultThisBinding(funcInfo.node, sourceCode);
}
// `this` at the top level of scripts always refers to the global object
if (funcInfo.isTopLevelOfScript || (!funcInfo.strict && funcInfo.defaultThis)) {
const grandParent = node.parent.parent;
if (
grandParent.type === 'CallExpression' &&
grandParent.arguments.length > 0 &&
// LWS BEGIN
astUtils.isSignatureCall(grandParent.arguments[0])
// LWS END
) {
return;
}
// `this.eval` is possible built-in `eval`.
report(node.parent);
}
},
};
},
};