UNPKG

@locker/eslint-plugin-unsafe-types

Version:
306 lines (269 loc) 11.4 kB
/** 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); } }, }; }, };