eslint-plugin-security 1.4.0 → 1.6.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.
Files changed (66) hide show
  1. package/.eslint-doc-generatorrc.js +9 -0
  2. package/.eslintrc +29 -1
  3. package/.github/ISSUE_TEMPLATE/bug-report.yml +85 -0
  4. package/.github/ISSUE_TEMPLATE/new-rule.yml +39 -0
  5. package/.github/ISSUE_TEMPLATE/rule-change.yml +61 -0
  6. package/.github/workflows/ci.yml +55 -0
  7. package/.github/workflows/pr.yml +19 -0
  8. package/.github/workflows/release-please.yml +39 -0
  9. package/.markdownlint.json +4 -0
  10. package/.markdownlintignore +3 -0
  11. package/.prettierrc.json +7 -0
  12. package/CHANGELOG.md +114 -34
  13. package/README.md +45 -85
  14. package/docs/avoid-command-injection-node.md +85 -0
  15. package/docs/bypass-connect-csrf-protection-by-abusing.md +42 -0
  16. package/docs/regular-expression-dos-and-node.md +83 -0
  17. package/docs/rules/detect-bidi-characters.md +50 -0
  18. package/docs/rules/detect-buffer-noassert.md +9 -0
  19. package/docs/rules/detect-child-process.md +9 -0
  20. package/docs/rules/detect-disable-mustache-escape.md +9 -0
  21. package/docs/rules/detect-eval-with-expression.md +7 -0
  22. package/docs/rules/detect-new-buffer.md +5 -0
  23. package/docs/rules/detect-no-csrf-before-method-override.md +9 -0
  24. package/docs/rules/detect-non-literal-fs-filename.md +7 -0
  25. package/docs/rules/detect-non-literal-regexp.md +7 -0
  26. package/docs/rules/detect-non-literal-require.md +7 -0
  27. package/docs/rules/detect-object-injection.md +7 -0
  28. package/docs/rules/detect-possible-timing-attacks.md +5 -0
  29. package/docs/rules/detect-pseudoRandomBytes.md +5 -0
  30. package/docs/rules/detect-unsafe-regex.md +7 -0
  31. package/docs/the-dangers-of-square-bracket-notation.md +107 -0
  32. package/index.js +10 -9
  33. package/package.json +34 -7
  34. package/rules/detect-bidi-characters.js +101 -0
  35. package/rules/detect-buffer-noassert.js +66 -55
  36. package/rules/detect-child-process.js +57 -25
  37. package/rules/detect-disable-mustache-escape.js +24 -14
  38. package/rules/detect-eval-with-expression.js +19 -9
  39. package/rules/detect-new-buffer.js +19 -16
  40. package/rules/detect-no-csrf-before-method-override.js +32 -25
  41. package/rules/detect-non-literal-fs-filename.js +86 -33
  42. package/rules/detect-non-literal-regexp.js +24 -18
  43. package/rules/detect-non-literal-require.js +25 -17
  44. package/rules/detect-object-injection.js +61 -59
  45. package/rules/detect-possible-timing-attacks.js +40 -42
  46. package/rules/detect-pseudoRandomBytes.js +18 -11
  47. package/rules/detect-unsafe-regex.js +36 -23
  48. package/test/detect-bidi-characters.js +74 -0
  49. package/test/detect-buffer-noassert.js +18 -18
  50. package/test/detect-child-process.js +49 -23
  51. package/test/detect-disable-mustache-escape.js +3 -4
  52. package/test/detect-eval-with-expression.js +4 -5
  53. package/test/detect-new-buffer.js +4 -5
  54. package/test/detect-no-csrf-before-method-override.js +3 -4
  55. package/test/detect-non-literal-fs-filename.js +135 -9
  56. package/test/detect-non-literal-regexp.js +5 -6
  57. package/test/detect-non-literal-require.js +11 -8
  58. package/test/detect-object-injection.js +3 -5
  59. package/test/detect-possible-timing-attacks.js +8 -10
  60. package/test/detect-pseudoRandomBytes.js +3 -4
  61. package/test/detect-unsafe-regexp.js +9 -11
  62. package/test/utils/import-utils.js +172 -0
  63. package/utils/data/fsFunctionData.json +51 -0
  64. package/utils/import-utils.js +196 -0
  65. package/.npmignore +0 -1
  66. package/rules/data/fsFunctionData.json +0 -51
@@ -0,0 +1,172 @@
1
+ 'use strict';
2
+
3
+ const { getImportAccessPath } = require('../../utils/import-utils');
4
+ const { deepStrictEqual } = require('assert');
5
+
6
+ const Linter = require('eslint').Linter;
7
+
8
+ function getGetImportAccessPathResult(code) {
9
+ const linter = new Linter();
10
+ const result = [];
11
+ linter.defineRule('test-rule', {
12
+ create(context) {
13
+ return {
14
+ 'Identifier[name = target]'(node) {
15
+ let expr = node;
16
+ if (node.parent.type === 'MemberExpression' && node.parent.property === node) {
17
+ expr = node.parent;
18
+ }
19
+ const info = getImportAccessPath({
20
+ node: expr,
21
+ scope: context.getScope(),
22
+ packageNames: ['target', 'target-foo', 'target-bar'],
23
+ });
24
+ if (!info) return;
25
+ result.push({
26
+ path: info.path,
27
+ packageName: info.packageName,
28
+ ...(info.defaultImport ? { defaultImport: info.defaultImport } : {}),
29
+ });
30
+ },
31
+ };
32
+ },
33
+ });
34
+
35
+ const linterResult = linter.verify(code, {
36
+ parserOptions: {
37
+ ecmaVersion: 6,
38
+ sourceType: 'module',
39
+ },
40
+ rules: {
41
+ 'test-rule': 'error',
42
+ },
43
+ });
44
+ deepStrictEqual(linterResult, []);
45
+
46
+ return result;
47
+ }
48
+
49
+ describe('getImportAccessPath', () => {
50
+ describe('The result of getImportAccessPath should be as expected.', () => {
51
+ for (const { code, result } of [
52
+ {
53
+ code: `var something = require('target');
54
+ something.target(c);`,
55
+ result: [
56
+ {
57
+ path: ['target'],
58
+ packageName: 'target',
59
+ },
60
+ ],
61
+ },
62
+ {
63
+ code: `var target = require('target');
64
+ target(c);
65
+ var { foo } = require('target-foo');
66
+ foo.target(c);
67
+ foo.bar.target(c);
68
+ var { a: bar } = require('target-bar');
69
+ bar.target(c);
70
+ var baz = require('target-baz');
71
+ baz.target(c);
72
+ var qux = qux.foo.target;`,
73
+ result: [
74
+ {
75
+ path: [],
76
+ packageName: 'target',
77
+ },
78
+ {
79
+ path: [],
80
+ packageName: 'target',
81
+ },
82
+ {
83
+ path: ['foo', 'target'],
84
+ packageName: 'target-foo',
85
+ },
86
+ {
87
+ path: ['foo', 'bar', 'target'],
88
+ packageName: 'target-foo',
89
+ },
90
+ {
91
+ path: ['a', 'target'],
92
+ packageName: 'target-bar',
93
+ },
94
+ ],
95
+ },
96
+ {
97
+ code: `require('target').target;
98
+ function fn () {
99
+ var { foo } = require('target-foo');
100
+ foo.target(c);
101
+ }`,
102
+ result: [
103
+ {
104
+ path: ['target'],
105
+ packageName: 'target',
106
+ },
107
+ {
108
+ path: ['foo', 'target'],
109
+ packageName: 'target-foo',
110
+ },
111
+ ],
112
+ },
113
+ {
114
+ code: `import { foo } from 'target-foo';
115
+ foo.target(c);
116
+ foo.bar.target(c);
117
+ import { a as bar } from 'target-bar';
118
+ bar.target(c);
119
+ import baz from 'target-baz';
120
+ baz.target(c);`,
121
+ result: [
122
+ {
123
+ path: ['foo', 'target'],
124
+ packageName: 'target-foo',
125
+ },
126
+ {
127
+ path: ['foo', 'bar', 'target'],
128
+ packageName: 'target-foo',
129
+ },
130
+ {
131
+ path: ['a', 'target'],
132
+ packageName: 'target-bar',
133
+ },
134
+ ],
135
+ },
136
+ {
137
+ code: `import foo from 'target-foo';
138
+ foo.target(c);
139
+ import * as bar from 'target-bar';
140
+ bar.target(c);`,
141
+ result: [
142
+ {
143
+ path: ['target'],
144
+ defaultImport: true,
145
+ packageName: 'target-foo',
146
+ },
147
+ {
148
+ path: ['target'],
149
+ packageName: 'target-bar',
150
+ },
151
+ ],
152
+ },
153
+ {
154
+ code: `import foo from 'target-foo';
155
+ function fn () {
156
+ foo.target(c);
157
+ }`,
158
+ result: [
159
+ {
160
+ path: ['target'],
161
+ defaultImport: true,
162
+ packageName: 'target-foo',
163
+ },
164
+ ],
165
+ },
166
+ ]) {
167
+ it(code, () => {
168
+ deepStrictEqual(getGetImportAccessPathResult(code), result);
169
+ });
170
+ }
171
+ });
172
+ });
@@ -0,0 +1,51 @@
1
+ {
2
+ "appendFile": [0],
3
+ "appendFileSync": [0],
4
+ "chmod": [0],
5
+ "chmodSync": [0],
6
+ "chown": [0],
7
+ "chownSync": [0],
8
+ "createReadStream": [0],
9
+ "createWriteStream": [0],
10
+ "exists": [0],
11
+ "existsSync": [0],
12
+ "lchmod": [0],
13
+ "lchmodSync": [0],
14
+ "lchown": [0],
15
+ "lchownSync": [0],
16
+ "link": [0, 1],
17
+ "linkSync": [0, 1],
18
+ "lstat": [0],
19
+ "lstatSync": [0],
20
+ "mkdir": [0],
21
+ "mkdirSync": [0],
22
+ "open": [0],
23
+ "openSync": [0],
24
+ "readdir": [0],
25
+ "readdirSync": [0],
26
+ "readFile": [0],
27
+ "readFileSync": [0],
28
+ "readlink": [0],
29
+ "readlinkSync": [0],
30
+ "realpath": [0],
31
+ "realpathSync": [0],
32
+ "rename": [0, 1],
33
+ "renameSync": [0, 1],
34
+ "rmdir": [0],
35
+ "rmdirSync": [0],
36
+ "stat": [0],
37
+ "statSync": [0],
38
+ "symlink": [0, 1],
39
+ "symlinkSync": [0, 1],
40
+ "truncate": [0],
41
+ "truncateSync": [0],
42
+ "unlink": [0],
43
+ "unlinkSync": [0],
44
+ "unwatchFile": [0],
45
+ "utimes": [0],
46
+ "utimesSync": [0],
47
+ "watch": [0],
48
+ "watchFile": [0],
49
+ "writeFile": [0],
50
+ "writeFileSync": [0]
51
+ }
@@ -0,0 +1,196 @@
1
+ module.exports.getImportAccessPath = getImportAccessPath;
2
+
3
+ /**
4
+ * @typedef {Object} ImportAccessPathInfo
5
+ * @property {string[]} path
6
+ * @property {boolean} [defaultImport]
7
+ * @property {string} packageName
8
+ * @property {import("estree").SimpleCallExpression | import("estree").ImportDeclaration} node
9
+ */
10
+ /**
11
+ * Returns the access path information from a require or import
12
+ *
13
+ * @param {Object} params
14
+ * @param {import("estree").Expression} params.node The node to check.
15
+ * @param {import("eslint").Scope.Scope} params.scope The scope of the given node.
16
+ * @param {string[]} params.packageNames The interesting packages the method is imported from
17
+ * @returns {ImportAccessPathInfo | null}
18
+ */
19
+ function getImportAccessPath({ node, scope, packageNames }) {
20
+ const tracked = new Set();
21
+ return getImportAccessPathInternal(node);
22
+
23
+ /**
24
+ * @param {import("estree").Expression} node
25
+ * @returns {ImportAccessPathInfo | null}
26
+ */
27
+ function getImportAccessPathInternal(node) {
28
+ if (tracked.has(node)) {
29
+ // Guard infinite loops.
30
+ return null;
31
+ }
32
+ tracked.add(node);
33
+
34
+ if (node.type === 'Identifier') {
35
+ // Track variables.
36
+ const variable = findVariable(scope, node.name);
37
+ if (!variable) {
38
+ return null;
39
+ }
40
+ // Check variables defined in `var foo = ...`.
41
+ const declDef = variable.defs.find(
42
+ /** @returns {def is import("eslint").Scope.Definition & {type: 'Variable'}} */
43
+ (def) => def.type === 'Variable' && def.node.type === 'VariableDeclarator' && def.node.init
44
+ );
45
+ if (declDef) {
46
+ let propName = null;
47
+ if (declDef.node.id.type === 'ObjectPattern') {
48
+ const property = declDef.node.id.properties.find((property) => property.type === 'Property' && property.value.type === 'Identifier' && property.value.name === node.name);
49
+ if (property && !property.computed) {
50
+ propName = property.key.name;
51
+ }
52
+ } else if (declDef.node.id.type !== 'Identifier') {
53
+ // Unknown access path
54
+ return null;
55
+ }
56
+ const nesting = getImportAccessPathInternal(declDef.node.init);
57
+ if (!nesting) {
58
+ return null;
59
+ }
60
+ /**
61
+ * Detects:
62
+ * | var something = require('package-name');
63
+ * | something(c);
64
+ * , or
65
+ * | var { propName: something } = require('package-name');
66
+ * | something(c);
67
+ */
68
+ return {
69
+ path: propName ? [...nesting.path, propName] : nesting.path,
70
+ defaultImport: nesting.defaultImport,
71
+ packageName: nesting.packageName,
72
+ node: nesting.node,
73
+ };
74
+ }
75
+ // Check variables defined in `import foo from ...`.
76
+ const importDef = variable.defs.find(
77
+ /** @returns {def is import("eslint").Scope.Definition & {type: 'ImportBinding'}} */
78
+ (def) =>
79
+ def.type === 'ImportBinding' &&
80
+ (def.node.type === 'ImportDefaultSpecifier' || def.node.type === 'ImportNamespaceSpecifier' || def.node.type === 'ImportSpecifier') &&
81
+ isImportDeclaration(def.node.parent)
82
+ );
83
+ if (importDef) {
84
+ let propName = null;
85
+ let defaultImport;
86
+ if (importDef.node.type === 'ImportSpecifier') {
87
+ propName = importDef.node.imported.name;
88
+ } else if (importDef.node.type === 'ImportDefaultSpecifier') {
89
+ defaultImport = true;
90
+ } else if (importDef.node.type !== 'ImportNamespaceSpecifier') {
91
+ // Unknown access path
92
+ return null;
93
+ }
94
+ /**
95
+ * Detects:
96
+ * | import { propName as something } from 'package-name';
97
+ * | something(c);
98
+ * ,
99
+ * | import * as something from 'package-name';
100
+ * | something(c);
101
+ * , or
102
+ * | import something from 'package-name';
103
+ * | something(c);
104
+ */
105
+ return {
106
+ path: propName ? [propName] : [],
107
+ defaultImport: defaultImport,
108
+ packageName: importDef.node.parent.source.value,
109
+ node: importDef.node.parent,
110
+ };
111
+ }
112
+ return null;
113
+ } else if (node.type === 'MemberExpression') {
114
+ if (node.computed) {
115
+ return null;
116
+ }
117
+ const nesting = getImportAccessPathInternal(node.object);
118
+ if (!nesting) {
119
+ return null;
120
+ }
121
+ /**
122
+ * Detects:
123
+ * | var something = require('package-name');
124
+ * | something.propName(c);
125
+ * ,
126
+ * | var { something } = require('package-name');
127
+ * | something.propName(c);
128
+ * ,
129
+ * | import something from 'package-name';
130
+ * | something.propName(c);
131
+ * ,
132
+ * | import * as something from 'package-name';
133
+ * | something.propName(c);
134
+ * , or
135
+ * | import { something } from 'package-name';
136
+ * | something.propName(c);
137
+ */
138
+ return {
139
+ path: [...nesting.path, node.property.name],
140
+ defaultImport: nesting.defaultImport,
141
+ packageName: nesting.packageName,
142
+ node: nesting.node,
143
+ };
144
+ } else if (isRequireBasedImport(node)) {
145
+ /**
146
+ * Detects:
147
+ * | require('package-name');
148
+ * ,
149
+ * | require('package-name').propName(c);
150
+ * , or
151
+ * | require('package-name')(c);
152
+ */
153
+ return {
154
+ path: [],
155
+ packageName: node.arguments[0].value,
156
+ node,
157
+ };
158
+ }
159
+ return null;
160
+ }
161
+
162
+ /**
163
+ * Checks whether the given expression node is a require based import, or not
164
+ * @param {import("estree").Expression} expression
165
+ */
166
+ function isRequireBasedImport(expression) {
167
+ return (
168
+ expression &&
169
+ expression.type === 'CallExpression' &&
170
+ expression.callee.name === 'require' &&
171
+ expression.arguments.length &&
172
+ expression.arguments[0].type === 'Literal' &&
173
+ packageNames.includes(expression.arguments[0].value)
174
+ );
175
+ }
176
+
177
+ /**
178
+ * Checks whether the given node is a import, or not
179
+ * @param {import("estree").Node} node
180
+ */
181
+ function isImportDeclaration(node) {
182
+ return node && node.type === 'ImportDeclaration' && packageNames.includes(node.source.value);
183
+ }
184
+ }
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
+ }
package/.npmignore DELETED
@@ -1 +0,0 @@
1
- node_modules
@@ -1,51 +0,0 @@
1
- {
2
- "appendFile": [0],
3
- "appendFileSync": [0],
4
- "chmod": [0],
5
- "chmodSync": [0],
6
- "chown": [0],
7
- "chownSync": [0],
8
- "createReadStream": [0],
9
- "createWriteStream": [0],
10
- "exists": [0],
11
- "existsSync": [0],
12
- "lchmod": [0],
13
- "lchmodSync": [0],
14
- "lchown": [0],
15
- "lchownSync": [0],
16
- "link": [0,1],
17
- "linkSync": [0,1],
18
- "lstat": [0],
19
- "lstatSync": [0],
20
- "mkdir": [0],
21
- "mkdirSync": [0],
22
- "open": [0],
23
- "openSync": [0],
24
- "readdir": [0],
25
- "readdirSync": [0],
26
- "readFile": [0],
27
- "readFileSync": [0],
28
- "readlink": [0],
29
- "readlinkSync": [0],
30
- "realpath": [0],
31
- "realpathSync": [0],
32
- "rename": [0,1],
33
- "renameSync": [0,1],
34
- "rmdir": [0],
35
- "rmdirSync": [0],
36
- "stat": [0],
37
- "statSync": [0],
38
- "symlink": [0,1],
39
- "symlinkSync": [0,1],
40
- "truncate": [0],
41
- "truncateSync": [0],
42
- "unlink": [0],
43
- "unlinkSync": [0],
44
- "unwatchFile": [0],
45
- "utimes": [0],
46
- "utimesSync": [0],
47
- "watch": [0],
48
- "watchFile": [0],
49
- "writeFile": [0],
50
- "writeFileSync": [0]
51
- }