eslint-plugin-security 1.7.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,7 +17,7 @@ jobs:
17
17
  persist-credentials: false
18
18
  - uses: actions/setup-node@v3
19
19
  with:
20
- node-version: '16.x'
20
+ node-version: 18
21
21
 
22
22
  - name: Install Packages
23
23
  run: npm install
@@ -30,12 +30,12 @@ jobs:
30
30
  strategy:
31
31
  matrix:
32
32
  os: [ubuntu-latest]
33
- node: [18.x, 16.x, 14.x, 12.x, '12.22.0']
33
+ node: [12.22.0, 12, 14, 16, 18, 20]
34
34
  include:
35
35
  - os: windows-latest
36
- node: '16.x'
36
+ node: 18
37
37
  - os: macOS-latest
38
- node: '16.x'
38
+ node: 18
39
39
  runs-on: ${{ matrix.os }}
40
40
  permissions:
41
41
  contents: read
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.0.0](https://www.github.com/eslint-community/eslint-plugin-security/compare/v1.7.1...v2.0.0) (2023-10-17)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * switch the recommended config to flat (#118)
9
+
10
+ ### Features
11
+
12
+ * switch the recommended config to flat ([#118](https://www.github.com/eslint-community/eslint-plugin-security/issues/118)) ([e20a366](https://www.github.com/eslint-community/eslint-plugin-security/commit/e20a3664c2f638466286ae9a97515722fc98f97c))
13
+
14
+ ### [1.7.1](https://www.github.com/eslint-community/eslint-plugin-security/compare/v1.7.0...v1.7.1) (2023-02-02)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * false positives for static expressions in detect-non-literal-fs-filename, detect-child-process, detect-non-literal-regexp, and detect-non-literal-require ([#109](https://www.github.com/eslint-community/eslint-plugin-security/issues/109)) ([56102b5](https://www.github.com/eslint-community/eslint-plugin-security/commit/56102b50aed4bd632dd668770eb37de58788110b))
20
+
3
21
  ## [1.7.0](https://www.github.com/eslint-community/eslint-plugin-security/compare/v1.6.0...v1.7.0) (2023-01-26)
4
22
 
5
23
 
package/README.md CHANGED
@@ -20,12 +20,12 @@ yarn add --dev eslint-plugin-security
20
20
 
21
21
  ## Usage
22
22
 
23
- Add the following to your `.eslintrc` file:
23
+ Add the following to your `eslint.config.js` file:
24
24
 
25
25
  ```js
26
- "extends": [
27
- "plugin:security/recommended"
28
- ]
26
+ const pluginSecurity = require('eslint-plugin-security');
27
+
28
+ module.exports = [pluginSecurity.configs.recommended];
29
29
  ```
30
30
 
31
31
  ## Developer guide
@@ -94,7 +94,7 @@ Well, yes and no. Is this particular vector a widespread problem? No, because cu
94
94
 
95
95
  Yes, we are talking about some fairly extreme edge cases, but don't make the assumption that your code doesn't have problems because of that - I have seen this issue in production code with some regularity. And, for the majority of node developers, a large portion of application code was not written by them, but rather included through required modules which may contain peculiar flaws like this one.
96
96
 
97
- Edge cases are uncommon, but because they are uncommon the problems with them are not well known, and they frequently go un-noticed during code review. If the code works, these types of problems tend to disappear. If the code works, and the problems are buried in a module nested n-levels deep, it's likely it won't be found until it causes problems, and by then it's too late. A blind require is essentially running untrusted code in your application. Be [aware of what you require.](https://requiresafe.com)
97
+ Edge cases are uncommon, but because they are uncommon the problems with them are not well known, and they frequently go un-noticed during code review. If the code works, these types of problems tend to disappear. If the code works, and the problems are buried in a module nested n-levels deep, it's likely it won't be found until it causes problems, and by then it's too late. A blind require is essentially running untrusted code in your application. Be aware of the code you're requiring.
98
98
 
99
99
  ## How do I fix it?
100
100
 
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ const jsPlugin = require('@eslint/js');
4
+ const prettierConfig = require('eslint-config-prettier');
5
+ const eslintPluginRecommendedConfig = require('eslint-plugin-eslint-plugin/configs/recommended');
6
+
7
+ const eslintPluginConfigs = [
8
+ eslintPluginRecommendedConfig,
9
+ {
10
+ rules: {
11
+ 'eslint-plugin/prefer-message-ids': 'off', // TODO: enable
12
+ 'eslint-plugin/require-meta-docs-description': ['error', { pattern: '^(Detects|Enforces|Requires|Disallows) .+\\.$' }],
13
+ 'eslint-plugin/require-meta-docs-url': [
14
+ 'error',
15
+ {
16
+ pattern: 'https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/{{name}}.md',
17
+ },
18
+ ],
19
+ 'eslint-plugin/require-meta-schema': 'off', // TODO: enable
20
+ 'eslint-plugin/require-meta-type': 'off', // TODO: enable
21
+ },
22
+ },
23
+ ];
24
+
25
+ module.exports = [
26
+ jsPlugin.configs.recommended,
27
+ prettierConfig,
28
+ ...eslintPluginConfigs,
29
+ {
30
+ languageOptions: {
31
+ sourceType: 'commonjs',
32
+ },
33
+ },
34
+ {
35
+ files: ['test/**/*.js'],
36
+ languageOptions: {
37
+ globals: {
38
+ describe: 'readonly',
39
+ it: 'readonly',
40
+ },
41
+ },
42
+ },
43
+ ];
package/index.js CHANGED
@@ -4,7 +4,13 @@
4
4
 
5
5
  'use strict';
6
6
 
7
- module.exports = {
7
+ const pkg = require('./package.json');
8
+
9
+ const plugin = {
10
+ meta: {
11
+ name: pkg.name,
12
+ version: pkg.version,
13
+ },
8
14
  rules: {
9
15
  'detect-unsafe-regex': require('./rules/detect-unsafe-regex'),
10
16
  'detect-non-literal-regexp': require('./rules/detect-non-literal-regexp'),
@@ -37,25 +43,29 @@ module.exports = {
37
43
  'detect-new-buffer': 0,
38
44
  'detect-bidi-characters': 0,
39
45
  },
40
- configs: {
41
- recommended: {
42
- plugins: ['security'],
43
- rules: {
44
- 'security/detect-buffer-noassert': 'warn',
45
- 'security/detect-child-process': 'warn',
46
- 'security/detect-disable-mustache-escape': 'warn',
47
- 'security/detect-eval-with-expression': 'warn',
48
- 'security/detect-new-buffer': 'warn',
49
- 'security/detect-no-csrf-before-method-override': 'warn',
50
- 'security/detect-non-literal-fs-filename': 'warn',
51
- 'security/detect-non-literal-regexp': 'warn',
52
- 'security/detect-non-literal-require': 'warn',
53
- 'security/detect-object-injection': 'warn',
54
- 'security/detect-possible-timing-attacks': 'warn',
55
- 'security/detect-pseudoRandomBytes': 'warn',
56
- 'security/detect-unsafe-regex': 'warn',
57
- 'security/detect-bidi-characters': 'warn',
58
- },
59
- },
46
+ configs: {}, // was assigned later so we can reference `plugin`
47
+ };
48
+
49
+ const recommended = {
50
+ plugins: { security: plugin },
51
+ rules: {
52
+ 'security/detect-buffer-noassert': 'warn',
53
+ 'security/detect-child-process': 'warn',
54
+ 'security/detect-disable-mustache-escape': 'warn',
55
+ 'security/detect-eval-with-expression': 'warn',
56
+ 'security/detect-new-buffer': 'warn',
57
+ 'security/detect-no-csrf-before-method-override': 'warn',
58
+ 'security/detect-non-literal-fs-filename': 'warn',
59
+ 'security/detect-non-literal-regexp': 'warn',
60
+ 'security/detect-non-literal-require': 'warn',
61
+ 'security/detect-object-injection': 'warn',
62
+ 'security/detect-possible-timing-attacks': 'warn',
63
+ 'security/detect-pseudoRandomBytes': 'warn',
64
+ 'security/detect-unsafe-regex': 'warn',
65
+ 'security/detect-bidi-characters': 'warn',
60
66
  },
61
67
  };
68
+
69
+ Object.assign(plugin.configs, { recommended });
70
+
71
+ module.exports = plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-security",
3
- "version": "1.7.0",
3
+ "version": "2.0.0",
4
4
  "description": "Security rules for eslint",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -46,12 +46,13 @@
46
46
  "safe-regex": "^2.1.1"
47
47
  },
48
48
  "devDependencies": {
49
+ "@eslint/js": "^8.51.0",
49
50
  "changelog": "1.3.0",
50
- "eslint": "^8.11.0",
51
+ "eslint": "^8.51.0",
51
52
  "eslint-config-nodesecurity": "^1.3.1",
52
53
  "eslint-config-prettier": "^8.5.0",
53
54
  "eslint-doc-generator": "^1.0.2",
54
- "eslint-plugin-eslint-plugin": "^5.0.2",
55
+ "eslint-plugin-eslint-plugin": "^5.1.1",
55
56
  "lint-staged": "^12.3.7",
56
57
  "markdownlint-cli": "^0.32.2",
57
58
  "mocha": "^9.2.2",
@@ -6,6 +6,7 @@
6
6
  'use strict';
7
7
 
8
8
  const { getImportAccessPath } = require('../utils/import-utils');
9
+ const { isStaticExpression } = require('../utils/is-static-expression');
9
10
  const childProcessPackageNames = ['child_process', 'node:child_process'];
10
11
 
11
12
  //------------------------------------------------------------------------------
@@ -41,7 +42,13 @@ module.exports = {
41
42
  }
42
43
 
43
44
  // Reports non-literal `exec()` calls.
44
- if (!node.arguments.length || node.arguments[0].type === 'Literal') {
45
+ if (
46
+ !node.arguments.length ||
47
+ isStaticExpression({
48
+ node: node.arguments[0],
49
+ scope: context.getScope(),
50
+ })
51
+ ) {
45
52
  return;
46
53
  }
47
54
  const pathInfo = getImportAccessPath({
@@ -10,21 +10,7 @@ const funcNames = Object.keys(fsMetaData);
10
10
  const fsPackageNames = ['fs', 'node:fs', 'fs/promises', 'node:fs/promises', 'fs-extra'];
11
11
 
12
12
  const { getImportAccessPath } = require('../utils/import-utils');
13
-
14
- //------------------------------------------------------------------------------
15
- // Utils
16
- //------------------------------------------------------------------------------
17
-
18
- function getIndices(node, argMeta) {
19
- return (argMeta || []).filter((argIndex) => node.arguments[argIndex].type !== 'Literal');
20
- }
21
-
22
- function generateReport({ context, node, packageName, methodName, indices }) {
23
- if (!indices || indices.length === 0) {
24
- return;
25
- }
26
- context.report({ node, message: `Found ${methodName} from package "${packageName}" with non literal argument at index ${indices.join(',')}` });
27
- }
13
+ const { isStaticExpression } = require('../utils/is-static-expression');
28
14
 
29
15
  //------------------------------------------------------------------------------
30
16
  // Rule Definition
@@ -87,15 +73,23 @@ module.exports = {
87
73
  }
88
74
  const packageName = pathInfo.packageName;
89
75
 
90
- const indices = getIndices(node, fsMetaData[fnName]);
91
-
92
- generateReport({
93
- context,
94
- node,
95
- packageName,
96
- methodName: fnName,
97
- indices,
98
- });
76
+ const indices = [];
77
+ for (const index of fsMetaData[fnName] || []) {
78
+ if (index >= node.arguments.length) {
79
+ continue;
80
+ }
81
+ const argument = node.arguments[index];
82
+ if (isStaticExpression({ node: argument, scope: context.getScope() })) {
83
+ continue;
84
+ }
85
+ indices.push(index);
86
+ }
87
+ if (indices.length) {
88
+ context.report({
89
+ node,
90
+ message: `Found ${fnName} from package "${packageName}" with non literal argument at index ${indices.join(',')}`,
91
+ });
92
+ }
99
93
  },
100
94
  };
101
95
  },
@@ -5,6 +5,8 @@
5
5
 
6
6
  'use strict';
7
7
 
8
+ const { isStaticExpression } = require('../utils/is-static-expression');
9
+
8
10
  //------------------------------------------------------------------------------
9
11
  // Rule Definition
10
12
  //------------------------------------------------------------------------------
@@ -24,7 +26,14 @@ module.exports = {
24
26
  NewExpression: function (node) {
25
27
  if (node.callee.name === 'RegExp') {
26
28
  const args = node.arguments;
27
- if (args && args.length > 0 && args[0].type !== 'Literal') {
29
+ if (
30
+ args &&
31
+ args.length > 0 &&
32
+ !isStaticExpression({
33
+ node: args[0],
34
+ scope: context.getScope(),
35
+ })
36
+ ) {
28
37
  return context.report({ node: node, message: 'Found non-literal argument to RegExp Constructor' });
29
38
  }
30
39
  }
@@ -5,6 +5,8 @@
5
5
 
6
6
  'use strict';
7
7
 
8
+ const { isStaticExpression } = require('../utils/is-static-expression');
9
+
8
10
  //------------------------------------------------------------------------------
9
11
  // Rule Definition
10
12
  //------------------------------------------------------------------------------
@@ -25,8 +27,12 @@ module.exports = {
25
27
  if (node.callee.name === 'require') {
26
28
  const args = node.arguments;
27
29
  if (
28
- (args && args.length > 0 && args[0].type === 'TemplateLiteral' && args[0].expressions.length > 0) ||
29
- (args[0].type !== 'TemplateLiteral' && args[0].type !== 'Literal')
30
+ args &&
31
+ args.length > 0 &&
32
+ !isStaticExpression({
33
+ node: args[0],
34
+ scope: context.getScope(),
35
+ })
30
36
  ) {
31
37
  return context.report({ node: node, message: 'Found non-literal argument in require' });
32
38
  }
@@ -50,6 +50,14 @@ tester.run(ruleName, rule, {
50
50
  function fn () {
51
51
  require('child_process').spawn(str)
52
52
  }`,
53
+ `
54
+ var child_process = require('child_process');
55
+ var FOO = 'ls';
56
+ child_process.exec(FOO);`,
57
+ `
58
+ import child_process from 'child_process';
59
+ const FOO = 'ls';
60
+ child_process.exec(FOO);`,
53
61
  ],
54
62
  invalid: [
55
63
  {
@@ -3,7 +3,7 @@
3
3
  const RuleTester = require('eslint').RuleTester;
4
4
  const tester = new RuleTester({
5
5
  parserOptions: {
6
- ecmaVersion: 6,
6
+ ecmaVersion: 13,
7
7
  sourceType: 'module',
8
8
  },
9
9
  });
@@ -24,6 +24,51 @@ tester.run(ruleName, require(`../rules/${ruleName}`), {
24
24
  code: `var something = require('fs').readFile, readFile = require('foo').readFile;
25
25
  readFile(c);`,
26
26
  },
27
+ {
28
+ code: `
29
+ import { promises as fsp } from 'fs';
30
+ import fs from 'fs';
31
+ import path from 'path';
32
+
33
+ const index = await fsp.readFile(path.resolve(__dirname, './index.html'), 'utf-8');
34
+ const key = fs.readFileSync(path.join(__dirname, './ssl.key'));
35
+ await fsp.writeFile(path.resolve(__dirname, './sitemap.xml'), sitemap);`,
36
+ globals: {
37
+ __dirname: 'readonly',
38
+ },
39
+ },
40
+ {
41
+ code: `
42
+ import fs from 'fs';
43
+ import path from 'path';
44
+ const dirname = path.dirname(__filename)
45
+ const key = fs.readFileSync(path.resolve(dirname, './index.html'));`,
46
+ globals: {
47
+ __filename: 'readonly',
48
+ },
49
+ },
50
+ {
51
+ code: `
52
+ import fs from 'fs';
53
+ const key = fs.readFileSync(\`\${process.cwd()}/path/to/foo.json\`);`,
54
+ globals: {
55
+ process: 'readonly',
56
+ },
57
+ },
58
+ `
59
+ import fs from 'fs';
60
+ import path from 'path';
61
+ import url from 'url';
62
+ const dirname = path.dirname(url.fileURLToPath(import.meta.url));
63
+ const html = fs.readFileSync(path.resolve(dirname, './index.html'), 'utf-8');`,
64
+ {
65
+ code: `
66
+ import fs from 'fs';
67
+ const pkg = fs.readFileSync(require.resolve('eslint/package.json'), 'utf-8');`,
68
+ globals: {
69
+ require: 'readonly',
70
+ },
71
+ },
27
72
  ],
28
73
  invalid: [
29
74
  /// requires
@@ -141,5 +186,15 @@ tester.run(ruleName, require(`../rules/${ruleName}`), {
141
186
  code: "var fs = require('fs');\nfunction foo () {\nvar { readFile: something } = fs.promises;\nsomething(filename);\n}",
142
187
  errors: [{ message: 'Found readFile from package "fs" with non literal argument at index 0' }],
143
188
  },
189
+ {
190
+ code: `
191
+ import fs from 'fs';
192
+ import path from 'path';
193
+ const key = fs.readFileSync(path.resolve(__dirname, foo));`,
194
+ globals: {
195
+ __filename: 'readonly',
196
+ },
197
+ errors: [{ message: 'Found readFileSync from package "fs" with non literal argument at index 0' }],
198
+ },
144
199
  ],
145
200
  });
@@ -7,7 +7,14 @@ const ruleName = 'detect-non-literal-regexp';
7
7
  const invalid = "var a = new RegExp(c, 'i')";
8
8
 
9
9
  tester.run(ruleName, require(`../rules/${ruleName}`), {
10
- valid: [{ code: "var a = new RegExp('ab+c', 'i')" }],
10
+ valid: [
11
+ { code: "var a = new RegExp('ab+c', 'i')" },
12
+ {
13
+ code: `
14
+ var source = 'ab+c'
15
+ var a = new RegExp(source, 'i')`,
16
+ },
17
+ ],
11
18
  invalid: [
12
19
  {
13
20
  code: invalid,
@@ -7,7 +7,21 @@ const tester = new RuleTester({ parserOptions: { ecmaVersion: 6 } });
7
7
  const ruleName = 'detect-non-literal-require';
8
8
 
9
9
  tester.run(ruleName, require(`../rules/${ruleName}`), {
10
- valid: [{ code: "var a = require('b')" }, { code: 'var a = require(`b`)' }],
10
+ valid: [
11
+ { code: "var a = require('b')" },
12
+ { code: 'var a = require(`b`)' },
13
+ {
14
+ code: `
15
+ const d = 'debounce'
16
+ var a = require(\`lodash/\${d}\`)`,
17
+ },
18
+ {
19
+ code: "const utils = require(__dirname + '/utils');",
20
+ globals: {
21
+ __dirname: 'readonly',
22
+ },
23
+ },
24
+ ],
11
25
  invalid: [
12
26
  {
13
27
  code: 'var a = require(c)',
@@ -0,0 +1,252 @@
1
+ 'use strict';
2
+
3
+ const { isStaticExpression } = require('../../utils/is-static-expression');
4
+ const { deepStrictEqual } = require('assert');
5
+
6
+ const Linter = require('eslint').Linter;
7
+
8
+ /**
9
+ * Get the return value using `isStaticExpression()`.
10
+ * Give `isStaticExpression()` the argument given to `target()` in the code as an expression.
11
+ */
12
+ function getIsStaticExpressionResult(code) {
13
+ const linter = new Linter();
14
+ const result = [];
15
+ linter.defineRule('test-rule', {
16
+ create(context) {
17
+ return {
18
+ 'CallExpression[callee.name = target]'(node) {
19
+ result.push(
20
+ ...node.arguments.map((expr) =>
21
+ isStaticExpression({
22
+ node: expr,
23
+ scope: context.getScope(),
24
+ })
25
+ )
26
+ );
27
+ },
28
+ };
29
+ },
30
+ });
31
+
32
+ const linterResult = linter.verify(code, {
33
+ parserOptions: {
34
+ ecmaVersion: 11,
35
+ sourceType: 'module',
36
+ },
37
+ globals: {
38
+ __dirname: 'readonly',
39
+ __filename: 'readonly',
40
+ require: 'readonly',
41
+ },
42
+ rules: {
43
+ 'test-rule': 'error',
44
+ },
45
+ });
46
+ deepStrictEqual(linterResult, []);
47
+
48
+ return result;
49
+ }
50
+
51
+ describe('isStaticExpression', () => {
52
+ describe('The result of isStaticExpression should be as expected.', () => {
53
+ for (const { code, result } of [
54
+ {
55
+ code: `target('foo');`,
56
+ result: [true],
57
+ },
58
+ {
59
+ code: `target(a);`,
60
+ result: [false],
61
+ },
62
+ {
63
+ code: `
64
+ const a = 'i'
65
+ target(a);`,
66
+ result: [true],
67
+ },
68
+ {
69
+ code: `
70
+ const a = b
71
+ target(a);`,
72
+ result: [false],
73
+ },
74
+ {
75
+ code: `
76
+ const a = a
77
+ target(a);`,
78
+ result: [false],
79
+ },
80
+ {
81
+ code: `
82
+ var a = 'foo'
83
+ var a = 'bar'
84
+ target(a);`,
85
+ result: [false],
86
+ },
87
+ {
88
+ code: `
89
+ var a = 'foo'
90
+ a = 'bar'
91
+ var b = 'bar'
92
+ target(a);
93
+ target(b);`,
94
+ result: [false, true],
95
+ },
96
+ {
97
+ code: `target(\`foo\`);`,
98
+ result: [true],
99
+ },
100
+ {
101
+ code: `
102
+ target(\`foo\${a}\`);`,
103
+ result: [false],
104
+ },
105
+ {
106
+ code: `
107
+ const a = 'i'
108
+ target(\`foo\${a}\`);`,
109
+ result: [true],
110
+ },
111
+ {
112
+ code: `
113
+ const a = 'i'
114
+ target('foo' + 'bar');
115
+ target(a + 'foo');
116
+ target('foo' + a + 'bar');
117
+ `,
118
+ result: [true, true, true],
119
+ },
120
+ {
121
+ code: `
122
+ const a = 'i'
123
+ target(b + 'bar');
124
+ target('foo' + a + b);
125
+ `,
126
+ result: [false, false],
127
+ },
128
+ {
129
+ code: `
130
+ target(__dirname, __filename);
131
+ `,
132
+ result: [true, true],
133
+ },
134
+ {
135
+ code: `
136
+ function fn(__dirname) {
137
+ target(__dirname, __filename);
138
+ }
139
+ `,
140
+ result: [false, true],
141
+ },
142
+ {
143
+ code: `
144
+ const __filename = a
145
+ target(__dirname, __filename);
146
+ `,
147
+ result: [true, false],
148
+ },
149
+ {
150
+ code: `
151
+ import path from 'path';
152
+ target(path.resolve(__dirname, './index.html'));
153
+ target(path.join(__dirname, './ssl.key'));
154
+ target(path.resolve(__dirname, './sitemap.xml'));
155
+ `,
156
+ result: [true, true, true],
157
+ },
158
+ {
159
+ code: `
160
+ import { posix as path } from 'path';
161
+ target(path.resolve(__dirname, './index.html'));
162
+ `,
163
+ result: [true],
164
+ },
165
+ {
166
+ code: `
167
+ const path = require('path');
168
+ target(path.resolve(__dirname, './index.html'));
169
+ `,
170
+ result: [true],
171
+ },
172
+ {
173
+ code: `
174
+ import path from 'unknown';
175
+ target(path.resolve(__dirname, './index.html'));
176
+ `,
177
+ result: [false],
178
+ },
179
+ {
180
+ code: `
181
+ import path from 'path';
182
+ target(path.unknown(__dirname, './index.html'));
183
+ `,
184
+ result: [false],
185
+ },
186
+ {
187
+ code: `
188
+ import path from 'path';
189
+ target(path.resolve.unknown(__dirname, './index.html'));
190
+ `,
191
+ result: [false],
192
+ },
193
+ {
194
+ code: `
195
+ import path from 'path';
196
+ const FOO = 'static'
197
+ target(path.resolve(__dirname, foo));
198
+ target(path.resolve(__dirname, FOO));
199
+ `,
200
+ result: [false, true],
201
+ },
202
+ {
203
+ code: `
204
+ import path from 'path';
205
+ const FOO = 'static'
206
+ target(__dirname + path.sep + foo);
207
+ target(__dirname + path.sep + FOO);
208
+ `,
209
+ result: [false, true],
210
+ },
211
+ {
212
+ code: `
213
+ target(require.resolve('static'));
214
+ target(require.resolve(foo));
215
+ `,
216
+ result: [true, false],
217
+ },
218
+ {
219
+ code: `
220
+ target(require);
221
+ target(require('static'));
222
+ `,
223
+ result: [false, false],
224
+ },
225
+ {
226
+ code: `
227
+ import url from "node:url";
228
+ import path from "node:path";
229
+
230
+ const filename = url.fileURLToPath(import.meta.url);
231
+ const dirname = path.dirname(url.fileURLToPath(import.meta.url));
232
+
233
+ target(filename);
234
+ target(dirname);
235
+ `,
236
+ result: [true, true],
237
+ },
238
+ {
239
+ code: `
240
+ import url from "node:url";
241
+ target(import.meta.url);
242
+ target(url.unknown(import.meta.url));
243
+ `,
244
+ result: [true, false],
245
+ },
246
+ ]) {
247
+ it(code, () => {
248
+ deepStrictEqual(getIsStaticExpressionResult(code), result);
249
+ });
250
+ }
251
+ });
252
+ });
@@ -0,0 +1,18 @@
1
+ module.exports.findVariable = findVariable;
2
+
3
+ /**
4
+ * Find the variable of a given name.
5
+ * @param {import("eslint").Scope.Scope} scope the scope to start finding
6
+ * @param {string} name the variable name to find.
7
+ * @returns {import("eslint").Scope.Variable | null}
8
+ */
9
+ function findVariable(scope, name) {
10
+ while (scope != null) {
11
+ const variable = scope.set.get(name);
12
+ if (variable != null) {
13
+ return variable;
14
+ }
15
+ scope = scope.upper;
16
+ }
17
+ return null;
18
+ }
@@ -1,3 +1,5 @@
1
+ const { findVariable } = require('./find-variable');
2
+
1
3
  module.exports.getImportAccessPath = getImportAccessPath;
2
4
 
3
5
  /**
@@ -182,15 +184,3 @@ function getImportAccessPath({ node, scope, packageNames }) {
182
184
  return node && node.type === 'ImportDeclaration' && packageNames.includes(node.source.value);
183
185
  }
184
186
  }
185
-
186
- /** @returns {import("eslint").Scope.Variable | null} */
187
- function findVariable(scope, name) {
188
- while (scope != null) {
189
- const variable = scope.set.get(name);
190
- if (variable != null) {
191
- return variable;
192
- }
193
- scope = scope.upper;
194
- }
195
- return null;
196
- }
@@ -0,0 +1,219 @@
1
+ const { findVariable } = require('./find-variable');
2
+ const { getImportAccessPath } = require('./import-utils');
3
+
4
+ module.exports.isStaticExpression = isStaticExpression;
5
+
6
+ const PATH_PACKAGE_NAMES = ['path', 'node:path', 'path/posix', 'node:path/posix'];
7
+ const URL_PACKAGE_NAMES = ['url', 'node:url'];
8
+ const PATH_CONSTRUCTION_METHOD_NAMES = new Set(['basename', 'dirname', 'extname', 'join', 'normalize', 'relative', 'resolve', 'toNamespacedPath']);
9
+ const PATH_STATIC_MEMBER_NAMES = new Set(['delimiter', 'sep']);
10
+
11
+ /**
12
+ * @type {WeakMap<import("estree").Expression, boolean>}
13
+ */
14
+ const cache = new WeakMap();
15
+
16
+ /**
17
+ * Checks whether the given expression node is a static or not.
18
+ *
19
+ * @param {Object} params
20
+ * @param {import("estree").Expression} params.node The node to check.
21
+ * @param {import("eslint").Scope.Scope} params.scope The scope of the given node.
22
+ * @returns {boolean} if true, the given expression node is a static.
23
+ */
24
+ function isStaticExpression({ node, scope }) {
25
+ const tracked = new Set();
26
+ return isStatic(node);
27
+
28
+ /**
29
+ * @param {import("estree").Expression} node
30
+ * @returns {boolean}
31
+ */
32
+ function isStatic(node) {
33
+ let result = cache.get(node);
34
+ if (result == null) {
35
+ result = isStaticWithoutCache(node);
36
+ cache.set(node, result);
37
+ }
38
+ return result;
39
+ }
40
+ /**
41
+ * @param {import("estree").Expression} node
42
+ * @returns {boolean}
43
+ */
44
+ function isStaticWithoutCache(node) {
45
+ if (tracked.has(node)) {
46
+ // Guard infinite loops.
47
+ return false;
48
+ }
49
+ tracked.add(node);
50
+ if (node.type === 'Literal') {
51
+ return true;
52
+ }
53
+ if (node.type === 'TemplateLiteral') {
54
+ // A node is static if all interpolations are static.
55
+ return node.expressions.every(isStatic);
56
+ }
57
+ if (node.type === 'BinaryExpression') {
58
+ // An expression is static if both operands are static.
59
+ return isStatic(node.left) && isStatic(node.right);
60
+ }
61
+ if (node.type === 'Identifier') {
62
+ const variable = findVariable(scope, node.name);
63
+ if (variable) {
64
+ if (variable.defs.length === 0) {
65
+ if (node.name === '__dirname' || node.name === '__filename') {
66
+ // It is a global variable that can be used in CJS of Node.js.
67
+ return true;
68
+ }
69
+ } else if (variable.defs.length === 1) {
70
+ const def = variable.defs[0];
71
+ if (
72
+ def.type === 'Variable' &&
73
+ // It has an initial value.
74
+ def.node.init &&
75
+ // It does not write new values.
76
+ variable.references.every((ref) => ref.isReadOnly() || ref.identifier === def.name)
77
+ ) {
78
+ // A variable is static if its initial value is static.
79
+ return isStatic(def.node.init);
80
+ }
81
+ }
82
+ } else {
83
+ return false;
84
+ }
85
+ }
86
+ return isStaticPath(node) || isStaticFileURLToPath(node) || isStaticImportMetaUrl(node) || isStaticRequireResolve(node) || isStaticCwd(node);
87
+ }
88
+
89
+ /**
90
+ * Checks whether the given expression is a static path construction.
91
+ *
92
+ * @param {import("estree").Expression} node The node to check.
93
+ * @returns {boolean} if true, the given expression is a static path construction.
94
+ */
95
+ function isStaticPath(node) {
96
+ const pathInfo = getImportAccessPath({
97
+ node: node.type === 'CallExpression' ? node.callee : node,
98
+ scope,
99
+ packageNames: PATH_PACKAGE_NAMES,
100
+ });
101
+ if (!pathInfo) {
102
+ return false;
103
+ }
104
+ /** @type {string | undefined} */
105
+ let name;
106
+ if (pathInfo.path.length === 1) {
107
+ // e.g. import path from 'path'
108
+ name = pathInfo.path[0];
109
+ } else if (pathInfo.path.length === 2 && pathInfo.path[0] === 'posix') {
110
+ // e.g. import { posix as path } from 'path'
111
+ name = pathInfo.path[1];
112
+ }
113
+ if (name == null) {
114
+ return false;
115
+ }
116
+
117
+ if (node.type === 'CallExpression') {
118
+ if (!PATH_CONSTRUCTION_METHOD_NAMES.has(name)) {
119
+ return false;
120
+ }
121
+ return Boolean(node.arguments.length) && node.arguments.every(isStatic);
122
+ }
123
+
124
+ return PATH_STATIC_MEMBER_NAMES.has(name);
125
+ }
126
+
127
+ /**
128
+ * Checks whether the given expression is a static `url.fileURLToPath()`.
129
+ *
130
+ * @param {import("estree").Expression} node The node to check.
131
+ * @returns {boolean} if true, the given expression is a static `url.fileURLToPath()`.
132
+ */
133
+ function isStaticFileURLToPath(node) {
134
+ if (node.type !== 'CallExpression') {
135
+ return false;
136
+ }
137
+ const pathInfo = getImportAccessPath({
138
+ node: node.callee,
139
+ scope,
140
+ packageNames: URL_PACKAGE_NAMES,
141
+ });
142
+ if (!pathInfo || pathInfo.path.length !== 1) {
143
+ return false;
144
+ }
145
+ let name = pathInfo.path[0];
146
+ if (name !== 'fileURLToPath') {
147
+ return false;
148
+ }
149
+ return Boolean(node.arguments.length) && node.arguments.every(isStatic);
150
+ }
151
+
152
+ /**
153
+ * Checks whether the given expression is an `import.meta.url`.
154
+ *
155
+ * @param {import("estree").Expression} node The node to check.
156
+ * @returns {boolean} if true, the given expression is an `import.meta.url`.
157
+ */
158
+ function isStaticImportMetaUrl(node) {
159
+ return (
160
+ node.type === 'MemberExpression' &&
161
+ !node.computed &&
162
+ node.property.type === 'Identifier' &&
163
+ node.property.name === 'url' &&
164
+ node.object.type === 'MetaProperty' &&
165
+ node.object.meta.name === 'import' &&
166
+ node.object.property.name === 'meta'
167
+ );
168
+ }
169
+
170
+ /**
171
+ * Checks whether the given expression is a static `require.resolve()`.
172
+ *
173
+ * @param {import("estree").Expression} node The node to check.
174
+ * @returns {boolean} if true, the given expression is a static `require.resolve()`.
175
+ */
176
+ function isStaticRequireResolve(node) {
177
+ if (
178
+ node.type !== 'CallExpression' ||
179
+ node.callee.type !== 'MemberExpression' ||
180
+ node.callee.computed ||
181
+ node.callee.property.type !== 'Identifier' ||
182
+ node.callee.property.name !== 'resolve' ||
183
+ node.callee.object.type !== 'Identifier' ||
184
+ node.callee.object.name !== 'require'
185
+ ) {
186
+ return false;
187
+ }
188
+ const variable = findVariable(scope, node.callee.object.name);
189
+ if (!variable || variable.defs.length !== 0) {
190
+ return false;
191
+ }
192
+ return Boolean(node.arguments.length) && node.arguments.every(isStatic);
193
+ }
194
+
195
+ /**
196
+ * Checks whether the given expression is a static `process.cwd()`.
197
+ *
198
+ * @param {import("estree").Expression} node The node to check.
199
+ * @returns {boolean} if true, the given expression is a static `process.cwd()`.
200
+ */
201
+ function isStaticCwd(node) {
202
+ if (
203
+ node.type !== 'CallExpression' ||
204
+ node.callee.type !== 'MemberExpression' ||
205
+ node.callee.computed ||
206
+ node.callee.property.type !== 'Identifier' ||
207
+ node.callee.property.name !== 'cwd' ||
208
+ node.callee.object.type !== 'Identifier' ||
209
+ node.callee.object.name !== 'process'
210
+ ) {
211
+ return false;
212
+ }
213
+ const variable = findVariable(scope, node.callee.object.name);
214
+ if (!variable || variable.defs.length !== 0) {
215
+ return false;
216
+ }
217
+ return true;
218
+ }
219
+ }
package/.eslintrc DELETED
@@ -1,31 +0,0 @@
1
- {
2
- "extends": ["eslint:recommended", "prettier", "plugin:eslint-plugin/recommended"],
3
- "parserOptions": {
4
- "ecmaVersion": "latest"
5
- },
6
- "env": {
7
- "node": true,
8
- "es2020": true
9
- },
10
- "rules": {
11
- "eslint-plugin/prefer-message-ids": "off", // TODO: enable
12
- "eslint-plugin/require-meta-docs-description": ["error", { "pattern": "^(Detects|Enforces|Requires|Disallows) .+\\.$" }],
13
- "eslint-plugin/require-meta-docs-url": [
14
- "error",
15
- {
16
- "pattern": "https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/{{name}}.md"
17
- }
18
- ],
19
- "eslint-plugin/require-meta-schema": "off", // TODO: enable
20
- "eslint-plugin/require-meta-type": "off" // TODO: enable
21
- },
22
- "overrides": [
23
- {
24
- "files": ["test/**/*.js"],
25
- "globals": {
26
- "describe": "readonly",
27
- "it": "readonly"
28
- }
29
- }
30
- ]
31
- }