From 254d276eaed6183e9a04a7b59d323709e7116558 Mon Sep 17 00:00:00 2001 From: Alexander T Date: Thu, 2 Jan 2020 03:26:20 +0200 Subject: [PATCH] feat(eslint-plugin): add no-implied-eval (#1375) --- packages/eslint-plugin/README.md | 1 + .../docs/rules/no-implied-eval.md | 95 +++ packages/eslint-plugin/src/configs/all.json | 1 + packages/eslint-plugin/src/rules/index.ts | 2 + .../src/rules/no-implied-eval.ts | 141 ++++ .../tests/rules/no-implied-eval.test.ts | 758 ++++++++++++++++++ 6 files changed, 998 insertions(+) create mode 100644 packages/eslint-plugin/docs/rules/no-implied-eval.md create mode 100644 packages/eslint-plugin/src/rules/no-implied-eval.ts create mode 100644 packages/eslint-plugin/tests/rules/no-implied-eval.test.ts diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 586d43d0aa0..722c2b4f3a2 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -125,6 +125,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/no-extraneous-class`](./docs/rules/no-extraneous-class.md) | Forbids the use of classes as namespaces | | | | | [`@typescript-eslint/no-floating-promises`](./docs/rules/no-floating-promises.md) | Requires Promise-like values to be handled appropriately. | | | :thought_balloon: | | [`@typescript-eslint/no-for-in-array`](./docs/rules/no-for-in-array.md) | Disallow iterating over an array with a for-in loop | :heavy_check_mark: | | :thought_balloon: | +| [`@typescript-eslint/no-implied-eval`](./docs/rules/no-implied-eval.md) | Disallow the use of `eval()`-like methods | | | :thought_balloon:| | [`@typescript-eslint/no-inferrable-types`](./docs/rules/no-inferrable-types.md) | Disallows explicit type declarations for variables or parameters initialized to a number, string, or boolean | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/no-magic-numbers`](./docs/rules/no-magic-numbers.md) | Disallows magic numbers | | | | | [`@typescript-eslint/no-misused-new`](./docs/rules/no-misused-new.md) | Enforce valid definition of `new` and `constructor` | :heavy_check_mark: | | | diff --git a/packages/eslint-plugin/docs/rules/no-implied-eval.md b/packages/eslint-plugin/docs/rules/no-implied-eval.md new file mode 100644 index 00000000000..741b6f6e7d1 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-implied-eval.md @@ -0,0 +1,95 @@ +# Disallow the use of `eval()`-like methods (`@typescript-eslint/no-implied-eval`) + +It's considered a good practice to avoid using `eval()`. There are security and performance implications involved with doing so, which is why many linters recommend disallowing `eval()`. However, there are some other ways to pass a string and have it interpreted as JavaScript code that have similar concerns. + +The first is using `setTimeout()`, `setInterval()`, `setImmediate` or `execScript()` (Internet Explorer only), all of which can accept a string of code as their first argument + +```ts +setTimeout('alert(`Hi!`);', 100); +``` + +or using `new Function()` + +```ts +const fn = new Function('a', 'b', 'return a + b'); +``` + +This is considered an implied `eval()` because a string of code is +passed in to be interpreted. The same can be done with `setInterval()`, `setImmediate()` and `execScript()`. All interpret the JavaScript code in the global scope. + +The best practice is to avoid using `new Function()` or `execScript()` and always use a function for the first argument of `setTimeout()`, `setInterval()` and `setImmediate()`. + +## Rule Details + +This rule aims to eliminate implied `eval()` through the use of `new Function()`, `setTimeout()`, `setInterval()`, `setImmediate()` or `execScript()`. + +Examples of **incorrect** code for this rule: + +```ts +/* eslint @typescript-eslint/no-implied-eval: "error" */ + +setTimeout('alert(`Hi!`);', 100); + +setInterval('alert(`Hi!`);', 100); + +setImmediate('alert(`Hi!`)'); + +execScript('alert(`Hi!`)'); + +window.setTimeout('count = 5', 10); + +window.setInterval('foo = bar', 10); + +const fn = '() = {}'; +setTimeout(fn, 100); + +const fn = () => { + return 'x = 10'; +}; +setTimeout(fn(), 100); + +const fn = new Function('a', 'b', 'return a + b'); +``` + +Examples of **correct** code for this rule: + +```ts +/* eslint @typescript-eslint/no-implied-eval: "error" */ + +setTimeout(function() { + alert('Hi!'); +}, 100); + +setInterval(function() { + alert('Hi!'); +}, 100); + +setImmediate(function() { + alert('Hi!'); +}); + +execScript(function() { + alert('Hi!'); +}); + +const fn = () => {}; +setTimeout(fn, 100); + +const foo = { + fn: function() {}, +}; +setTimeout(foo.fn, 100); +setTimeout(foo.fn.bind(this), 100); + +class Foo { + static fn = () => {}; +} + +setTimeout(Foo.fn, 100); +``` + +## When Not To Use It + +If you want to allow `new Function()` or `setTimeout()`, `setInterval()`, `setImmediate()` and `execScript()` with string arguments, then you can safely disable this rule. + +Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/no-implied-eval.md) diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index b350f86de39..f9eed1fca9c 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -39,6 +39,7 @@ "@typescript-eslint/no-extraneous-class": "error", "@typescript-eslint/no-floating-promises": "error", "@typescript-eslint/no-for-in-array": "error", + "@typescript-eslint/no-implied-eval": "error", "@typescript-eslint/no-inferrable-types": "error", "no-magic-numbers": "off", "@typescript-eslint/no-magic-numbers": "error", diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index a14884bbbd8..5d2f53cce25 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -28,6 +28,7 @@ import noExtraSemi from './no-extra-semi'; import noExtraneousClass from './no-extraneous-class'; import noFloatingPromises from './no-floating-promises'; import noForInArray from './no-for-in-array'; +import noImpliedEval from './no-implied-eval'; import noInferrableTypes from './no-inferrable-types'; import noMagicNumbers from './no-magic-numbers'; import noMisusedNew from './no-misused-new'; @@ -107,6 +108,7 @@ export default { 'no-floating-promises': noFloatingPromises, 'no-for-in-array': noForInArray, 'no-inferrable-types': noInferrableTypes, + 'no-implied-eval': noImpliedEval, 'no-magic-numbers': noMagicNumbers, 'no-misused-new': noMisusedNew, 'no-misused-promises': noMisusedPromises, diff --git a/packages/eslint-plugin/src/rules/no-implied-eval.ts b/packages/eslint-plugin/src/rules/no-implied-eval.ts new file mode 100644 index 00000000000..fe50beba050 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-implied-eval.ts @@ -0,0 +1,141 @@ +import * as ts from 'typescript'; +import { + TSESTree, + AST_NODE_TYPES, +} from '@typescript-eslint/experimental-utils'; +import * as util from '../util'; + +const FUNCTION_CONSTRUCTOR = 'Function'; +const EVAL_LIKE_METHODS = new Set([ + 'setImmediate', + 'setInterval', + 'setTimeout', + 'execScript', +]); + +export default util.createRule({ + name: 'no-implied-eval', + meta: { + docs: { + description: 'Disallow the use of `eval()`-like methods', + category: 'Best Practices', + recommended: false, + requiresTypeChecking: true, + }, + messages: { + noImpliedEvalError: 'Implied eval. Consider passing a function.', + noFunctionConstructor: + 'Implied eval. Do not use the Function constructor to create functions.', + }, + schema: [], + type: 'suggestion', + }, + defaultOptions: [], + create(context) { + const parserServices = util.getParserServices(context); + const checker = parserServices.program.getTypeChecker(); + + function getCalleeName( + node: TSESTree.LeftHandSideExpression, + ): string | null { + if (node.type === AST_NODE_TYPES.Identifier) { + return node.name; + } + + if ( + node.type === AST_NODE_TYPES.MemberExpression && + node.object.type === AST_NODE_TYPES.Identifier && + node.object.name === 'window' + ) { + if (node.property.type === AST_NODE_TYPES.Identifier) { + return node.property.name; + } + + if ( + node.property.type === AST_NODE_TYPES.Literal && + typeof node.property.value === 'string' + ) { + return node.property.value; + } + } + + return null; + } + + function isFunctionType(node: TSESTree.Node): boolean { + const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node); + const type = checker.getTypeAtLocation(tsNode); + const symbol = type.getSymbol(); + + if ( + symbol?.flags === ts.SymbolFlags.Function || + symbol?.flags === ts.SymbolFlags.Method + ) { + return true; + } + + const signatures = checker.getSignaturesOfType( + type, + ts.SignatureKind.Call, + ); + + if (signatures.length) { + const [{ declaration }] = signatures; + return declaration?.kind === ts.SyntaxKind.FunctionType; + } + + return false; + } + + function isFunction(node: TSESTree.Node): boolean { + switch (node.type) { + case AST_NODE_TYPES.ArrowFunctionExpression: + case AST_NODE_TYPES.FunctionDeclaration: + case AST_NODE_TYPES.FunctionExpression: + return true; + + case AST_NODE_TYPES.MemberExpression: + case AST_NODE_TYPES.Identifier: + return isFunctionType(node); + + case AST_NODE_TYPES.CallExpression: + return ( + (node.callee.type === AST_NODE_TYPES.Identifier && + node.callee.name === 'bind') || + isFunctionType(node) + ); + + default: + return false; + } + } + + function checkImpliedEval( + node: TSESTree.NewExpression | TSESTree.CallExpression, + ): void { + const calleeName = getCalleeName(node.callee); + if (calleeName === null) { + return; + } + + if (calleeName === FUNCTION_CONSTRUCTOR) { + context.report({ node, messageId: 'noFunctionConstructor' }); + return; + } + + if (node.arguments.length === 0) { + return; + } + + const [handler] = node.arguments; + if (EVAL_LIKE_METHODS.has(calleeName) && !isFunction(handler)) { + context.report({ node: handler, messageId: 'noImpliedEvalError' }); + } + } + + return { + NewExpression: checkImpliedEval, + CallExpression: checkImpliedEval, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/no-implied-eval.test.ts b/packages/eslint-plugin/tests/rules/no-implied-eval.test.ts new file mode 100644 index 00000000000..947044727d5 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-implied-eval.test.ts @@ -0,0 +1,758 @@ +import rule from '../../src/rules/no-implied-eval'; +import { RuleTester, getFixturesRootDir } from '../RuleTester'; + +const rootDir = getFixturesRootDir(); +const ruleTester = new RuleTester({ + parserOptions: { + tsconfigRootDir: rootDir, + ecmaVersion: 2015, + project: './tsconfig.json', + }, + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('no-implied-eval', rule, { + valid: [ + `foo.setImmediate(null);`, + `foo.setInterval(null);`, + `foo.execScript(null);`, + `foo.setTimeout(null);`, + `foo()`, + `(function(){ })()`, + + `setTimeout(() => {}, 0);`, + `window.setTimeout(() => {}, 0);`, + `window['setTimeout'](() => {}, 0);`, + + `setInterval(() => {}, 0);`, + `window.setInterval(() => {}, 0);`, + `window['setInterval'](() => {}, 0);`, + + `setImmediate(() => {});`, + `window.setImmediate(() => {});`, + `window['setImmediate'](() => {});`, + + `execScript(() => {});`, + `window.execScript(() => {});`, + `window['execScript'](() => {});`, + + { + code: ` +const foo = () => {}; + +setTimeout(foo, 0); +setInterval(foo, 0); +setImmediate(foo); +execScript(foo); + `, + }, + { + code: ` +const foo = function () {}; + +setTimeout(foo, 0); +setInterval(foo, 0); +setImmediate(foo); +execScript(foo); + `, + }, + { + code: ` +function foo() {}; + +setTimeout(foo, 0); +setInterval(foo, 0); +setImmediate(foo); +execScript(foo); + `, + }, + { + code: ` +const foo = { + fn: () => {}, +} + +setTimeout(foo.fn, 0); +setInterval(foo.fn, 0); +setImmediate(foo.fn); +execScript(foo.fn); + `, + }, + { + code: ` +const foo = { + fn: function () {}, +} + +setTimeout(foo.fn, 0); +setInterval(foo.fn, 0); +setImmediate(foo.fn); +execScript(foo.fn); + `, + }, + { + code: ` +const foo = { + fn: function foo() {}, +} + +setTimeout(foo.fn, 0); +setInterval(foo.fn, 0); +setImmediate(foo.fn); +execScript(foo.fn); + `, + }, + { + code: ` +const foo = { + fn() {}, +} + +setTimeout(foo.fn, 0); +setInterval(foo.fn, 0); +setImmediate(foo.fn); +execScript(foo.fn); + `, + }, + { + code: ` +const foo = { + fn: () => {}, +} +const fn = 'fn'; + +setTimeout(foo[fn], 0); +setInterval(foo[fn], 0); +setImmediate(foo[fn]); +execScript(foo[fn]); + `, + }, + { + code: ` +const foo = { + fn: () => {}, +} + +setTimeout(foo['fn'], 0); +setInterval(foo['fn'], 0); +setImmediate(foo['fn']); +execScript(foo['fn']); + `, + }, + { + code: ` +const foo: () => void = () => { +}; + +setTimeout(foo, 0); +setInterval(foo, 0); +setImmediate(foo); +execScript(foo); + `, + }, + { + code: ` +const foo: (() => () => void) = () => { + return () => {}; +} + +setTimeout(foo(), 0); +setInterval(foo(), 0); +setImmediate(foo()); +execScript(foo()); + `, + }, + { + code: ` +const foo: (() => () => void) = () => () => {}; + +setTimeout(foo(), 0); +setInterval(foo(), 0); +setImmediate(foo()); +execScript(foo()); + `, + }, + { + code: ` +const foo = () => () => {}; + +setTimeout(foo(), 0); +setInterval(foo(), 0); +setImmediate(foo()); +execScript(foo()); + `, + }, + { + code: ` +const foo = function foo () { + return function foo() {} +} + +setTimeout(foo(), 0); +setInterval(foo(), 0); +setImmediate(foo()); +execScript(foo()); + `, + }, + { + code: ` +const foo = function () { + return function () { + return ''; + } +} + +setTimeout(foo(), 0); +setInterval(foo(), 0); +setImmediate(foo()); +execScript(foo()); + `, + }, + { + code: ` +const foo: (() => () => void) = function foo () { + return function foo() {} +} + +setTimeout(foo(), 0); +setInterval(foo(), 0); +setImmediate(foo()); +execScript(foo()); + `, + }, + { + code: ` +function foo() { + return function foo() { + return () => {}; + }; +} + +setTimeout(foo()(), 0); +setInterval(foo()(), 0); +setImmediate(foo()()); +execScript(foo()()); + `, + }, + { + code: ` +class Foo { + static fn = () => {}; +} + +setTimeout(Foo.fn, 0); +setInterval(Foo.fn, 0); +setImmediate(Foo.fn); +execScript(Foo.fn); + `, + }, + { + code: ` +class Foo { + fn() {} +} + +const foo = new Foo(); + +setTimeout(foo.fn, 0); +setInterval(foo.fn, 0); +setImmediate(foo.fn); +execScript(foo.fn); + `, + }, + { + code: ` +class Foo { + fn() {} +} +const foo = new Foo(); +const fn = foo.fn; + +setTimeout(fn.bind(null), 0); +setInterval(fn.bind(null), 0); +setImmediate(fn.bind(null)); +execScript(fn.bind(null)); + `, + }, + { + code: ` +const fn = (foo: () => void) => { + setTimeout(foo, 0); + setInterval(foo, 0); + setImmediate(foo); + execScript(foo); +} + `, + }, + ], + + invalid: [ + { + code: ` +setTimeout('x = 1', 0); +setInterval('x = 1', 0); +setImmediate('x = 1'); +execScript('x = 1'); + `, + errors: [ + { + messageId: 'noImpliedEvalError', + line: 2, + column: 12, + }, + { + messageId: 'noImpliedEvalError', + line: 3, + column: 13, + }, + { + messageId: 'noImpliedEvalError', + line: 4, + column: 14, + }, + { + messageId: 'noImpliedEvalError', + line: 5, + column: 12, + }, + ], + }, + { + code: ` +setTimeout(undefined, 0); +setInterval(undefined, 0); +setImmediate(undefined); +execScript(undefined); + `, + errors: [ + { + messageId: 'noImpliedEvalError', + line: 2, + column: 12, + }, + { + messageId: 'noImpliedEvalError', + line: 3, + column: 13, + }, + { + messageId: 'noImpliedEvalError', + line: 4, + column: 14, + }, + { + messageId: 'noImpliedEvalError', + line: 5, + column: 12, + }, + ], + }, + { + code: ` +setTimeout(1 + '' + (() => {}), 0); +setInterval(1 + '' + (() => {}), 0); +setImmediate(1 + '' + (() => {})); +execScript(1 + '' + (() => {})); + `, + errors: [ + { + messageId: 'noImpliedEvalError', + line: 2, + column: 12, + }, + { + messageId: 'noImpliedEvalError', + line: 3, + column: 13, + }, + { + messageId: 'noImpliedEvalError', + line: 4, + column: 14, + }, + { + messageId: 'noImpliedEvalError', + line: 5, + column: 12, + }, + ], + }, + { + code: ` +const foo = 'x = 1'; + +setTimeout(foo, 0); +setInterval(foo, 0); +setImmediate(foo); +execScript(foo); + `, + errors: [ + { + messageId: 'noImpliedEvalError', + line: 4, + column: 12, + }, + { + messageId: 'noImpliedEvalError', + line: 5, + column: 13, + }, + { + messageId: 'noImpliedEvalError', + line: 6, + column: 14, + }, + { + messageId: 'noImpliedEvalError', + line: 7, + column: 12, + }, + ], + }, + { + code: ` +const foo = function () { + return 'x + 1'; +}; + +setTimeout(foo(), 0); +setInterval(foo(), 0); +setImmediate(foo()); +execScript(foo()); + `, + errors: [ + { + messageId: 'noImpliedEvalError', + line: 6, + column: 12, + }, + { + messageId: 'noImpliedEvalError', + line: 7, + column: 13, + }, + { + messageId: 'noImpliedEvalError', + line: 8, + column: 14, + }, + { + messageId: 'noImpliedEvalError', + line: 9, + column: 12, + }, + ], + }, + { + code: ` +const foo = function () { + return () => 'x + 1'; +}; + +setTimeout(foo()(), 0); +setInterval(foo()(), 0); +setImmediate(foo()()); +execScript(foo()()); + `, + errors: [ + { + messageId: 'noImpliedEvalError', + line: 6, + column: 12, + }, + { + messageId: 'noImpliedEvalError', + line: 7, + column: 13, + }, + { + messageId: 'noImpliedEvalError', + line: 8, + column: 14, + }, + { + messageId: 'noImpliedEvalError', + line: 9, + column: 12, + }, + ], + }, + { + code: ` +const fn = function () {} + +setTimeout(fn + '', 0); +setInterval(fn + '', 0); +setImmediate(fn + ''); +execScript(fn + ''); + `, + errors: [ + { + messageId: 'noImpliedEvalError', + line: 4, + column: 12, + }, + { + messageId: 'noImpliedEvalError', + line: 5, + column: 13, + }, + { + messageId: 'noImpliedEvalError', + line: 6, + column: 14, + }, + { + messageId: 'noImpliedEvalError', + line: 7, + column: 12, + }, + ], + }, + { + code: ` +const foo: string = 'x + 1'; + +setTimeout(foo, 0); +setInterval(foo, 0); +setImmediate(foo); +execScript(foo); + `, + errors: [ + { + messageId: 'noImpliedEvalError', + line: 4, + column: 12, + }, + { + messageId: 'noImpliedEvalError', + line: 5, + column: 13, + }, + { + messageId: 'noImpliedEvalError', + line: 6, + column: 14, + }, + { + messageId: 'noImpliedEvalError', + line: 7, + column: 12, + }, + ], + }, + { + code: ` +const foo = new String('x + 1'); + +setTimeout(foo, 0); +setInterval(foo, 0); +setImmediate(foo); +execScript(foo); + `, + errors: [ + { + messageId: 'noImpliedEvalError', + line: 4, + column: 12, + }, + { + messageId: 'noImpliedEvalError', + line: 5, + column: 13, + }, + { + messageId: 'noImpliedEvalError', + line: 6, + column: 14, + }, + { + messageId: 'noImpliedEvalError', + line: 7, + column: 12, + }, + ], + }, + { + code: ` +const foo = 'x + 1'; + +setTimeout(foo as any, 0); +setInterval(foo as any, 0); +setImmediate(foo as any); +execScript(foo as any); + `, + errors: [ + { + messageId: 'noImpliedEvalError', + line: 4, + column: 12, + }, + { + messageId: 'noImpliedEvalError', + line: 5, + column: 13, + }, + { + messageId: 'noImpliedEvalError', + line: 6, + column: 14, + }, + { + messageId: 'noImpliedEvalError', + line: 7, + column: 12, + }, + ], + }, + { + code: ` +const fn = (foo: string | any) => { + setTimeout(foo, 0); + setInterval(foo, 0); + setImmediate(foo); + execScript(foo); +} + `, + errors: [ + { + messageId: 'noImpliedEvalError', + line: 3, + column: 14, + }, + { + messageId: 'noImpliedEvalError', + line: 4, + column: 15, + }, + { + messageId: 'noImpliedEvalError', + line: 5, + column: 16, + }, + { + messageId: 'noImpliedEvalError', + line: 6, + column: 14, + }, + ], + }, + { + code: ` +window.setTimeout(\`\`, 0); +window['setTimeout'](\`\`, 0); + +window.setInterval(\`\`, 0); +window['setInterval'](\`\`, 0); + +window.setImmediate(\`\`); +window['setImmediate'](\`\`); + +window.execScript(\`\`); +window['execScript'](\`\`); + `, + errors: [ + { + messageId: 'noImpliedEvalError', + line: 2, + column: 19, + }, + { + messageId: 'noImpliedEvalError', + line: 3, + column: 22, + }, + { + messageId: 'noImpliedEvalError', + line: 5, + column: 20, + }, + { + messageId: 'noImpliedEvalError', + line: 6, + column: 23, + }, + { + messageId: 'noImpliedEvalError', + line: 8, + column: 21, + }, + { + messageId: 'noImpliedEvalError', + line: 9, + column: 24, + }, + { + messageId: 'noImpliedEvalError', + line: 11, + column: 19, + }, + { + messageId: 'noImpliedEvalError', + line: 12, + column: 22, + }, + ], + }, + { + code: `const fn = Function()`, + errors: [ + { + messageId: 'noFunctionConstructor', + line: 1, + column: 12, + }, + ], + }, + { + code: `const fn = new Function('a', 'b', 'return a + b');`, + errors: [ + { + messageId: 'noFunctionConstructor', + line: 1, + column: 12, + }, + ], + }, + { + code: `const fn = window.Function();`, + errors: [ + { + messageId: 'noFunctionConstructor', + line: 1, + column: 12, + }, + ], + }, + { + code: `const fn = new window.Function();`, + errors: [ + { + messageId: 'noFunctionConstructor', + line: 1, + column: 12, + }, + ], + }, + { + code: `const fn = window['Function']();`, + errors: [ + { + messageId: 'noFunctionConstructor', + line: 1, + column: 12, + }, + ], + }, + { + code: `const fn = new window['Function']();`, + errors: [ + { + messageId: 'noFunctionConstructor', + line: 1, + column: 12, + }, + ], + }, + ], +});