no-implied-eval.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. /**
  2. * @fileoverview Rule to flag use of implied eval via setTimeout and setInterval
  3. * @author James Allardice
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Rule Definition
  8. //------------------------------------------------------------------------------
  9. module.exports = {
  10. meta: {
  11. type: "suggestion",
  12. docs: {
  13. description: "disallow the use of `eval()`-like methods",
  14. category: "Best Practices",
  15. recommended: false,
  16. url: "https://eslint.org/docs/rules/no-implied-eval"
  17. },
  18. schema: []
  19. },
  20. create(context) {
  21. const CALLEE_RE = /^(setTimeout|setInterval|execScript)$/u;
  22. /*
  23. * Figures out if we should inspect a given binary expression. Is a stack
  24. * of stacks, where the first element in each substack is a CallExpression.
  25. */
  26. const impliedEvalAncestorsStack = [];
  27. //--------------------------------------------------------------------------
  28. // Helpers
  29. //--------------------------------------------------------------------------
  30. /**
  31. * Get the last element of an array, without modifying arr, like pop(), but non-destructive.
  32. * @param {Array} arr What to inspect
  33. * @returns {*} The last element of arr
  34. * @private
  35. */
  36. function last(arr) {
  37. return arr ? arr[arr.length - 1] : null;
  38. }
  39. /**
  40. * Checks if the given MemberExpression node is a potentially implied eval identifier on window.
  41. * @param {ASTNode} node The MemberExpression node to check.
  42. * @returns {boolean} Whether or not the given node is potentially an implied eval.
  43. * @private
  44. */
  45. function isImpliedEvalMemberExpression(node) {
  46. const object = node.object,
  47. property = node.property,
  48. hasImpliedEvalName = CALLEE_RE.test(property.name) || CALLEE_RE.test(property.value);
  49. return object.name === "window" && hasImpliedEvalName;
  50. }
  51. /**
  52. * Determines if a node represents a call to a potentially implied eval.
  53. *
  54. * This checks the callee name and that there's an argument, but not the type of the argument.
  55. * @param {ASTNode} node The CallExpression to check.
  56. * @returns {boolean} True if the node matches, false if not.
  57. * @private
  58. */
  59. function isImpliedEvalCallExpression(node) {
  60. const isMemberExpression = (node.callee.type === "MemberExpression"),
  61. isIdentifier = (node.callee.type === "Identifier"),
  62. isImpliedEvalCallee =
  63. (isIdentifier && CALLEE_RE.test(node.callee.name)) ||
  64. (isMemberExpression && isImpliedEvalMemberExpression(node.callee));
  65. return isImpliedEvalCallee && node.arguments.length;
  66. }
  67. /**
  68. * Checks that the parent is a direct descendent of an potential implied eval CallExpression, and if the parent is a CallExpression, that we're the first argument.
  69. * @param {ASTNode} node The node to inspect the parent of.
  70. * @returns {boolean} Was the parent a direct descendent, and is the child therefore potentially part of a dangerous argument?
  71. * @private
  72. */
  73. function hasImpliedEvalParent(node) {
  74. // make sure our parent is marked
  75. return node.parent === last(last(impliedEvalAncestorsStack)) &&
  76. // if our parent is a CallExpression, make sure we're the first argument
  77. (node.parent.type !== "CallExpression" || node === node.parent.arguments[0]);
  78. }
  79. /**
  80. * Checks if our parent is marked as part of an implied eval argument. If
  81. * so, collapses the top of impliedEvalAncestorsStack and reports on the
  82. * original CallExpression.
  83. * @param {ASTNode} node The CallExpression to check.
  84. * @returns {boolean} True if the node matches, false if not.
  85. * @private
  86. */
  87. function checkString(node) {
  88. if (hasImpliedEvalParent(node)) {
  89. // remove the entire substack, to avoid duplicate reports
  90. const substack = impliedEvalAncestorsStack.pop();
  91. context.report({ node: substack[0], message: "Implied eval. Consider passing a function instead of a string." });
  92. }
  93. }
  94. //--------------------------------------------------------------------------
  95. // Public
  96. //--------------------------------------------------------------------------
  97. return {
  98. CallExpression(node) {
  99. if (isImpliedEvalCallExpression(node)) {
  100. // call expressions create a new substack
  101. impliedEvalAncestorsStack.push([node]);
  102. }
  103. },
  104. "CallExpression:exit"(node) {
  105. if (node === last(last(impliedEvalAncestorsStack))) {
  106. /*
  107. * Destroys the entire sub-stack, rather than just using
  108. * last(impliedEvalAncestorsStack).pop(), as a CallExpression is
  109. * always the bottom of a impliedEvalAncestorsStack substack.
  110. */
  111. impliedEvalAncestorsStack.pop();
  112. }
  113. },
  114. BinaryExpression(node) {
  115. if (node.operator === "+" && hasImpliedEvalParent(node)) {
  116. last(impliedEvalAncestorsStack).push(node);
  117. }
  118. },
  119. "BinaryExpression:exit"(node) {
  120. if (node === last(last(impliedEvalAncestorsStack))) {
  121. last(impliedEvalAncestorsStack).pop();
  122. }
  123. },
  124. Literal(node) {
  125. if (typeof node.value === "string") {
  126. checkString(node);
  127. }
  128. },
  129. TemplateLiteral(node) {
  130. checkString(node);
  131. }
  132. };
  133. }
  134. };