eslint-plugin-node-security 4.8.0 → 4.9.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 (42) hide show
  1. package/README.md +1 -1
  2. package/package.json +3 -3
  3. package/src/index.js +1 -95
  4. package/src/oxlint.js +1 -3
  5. package/src/rules/detect-child-process/index.js +1 -625
  6. package/src/rules/detect-eval-with-expression/index.js +1 -364
  7. package/src/rules/detect-non-literal-fs-filename/index.js +1 -516
  8. package/src/rules/detect-suspicious-dependencies/index.js +1 -69
  9. package/src/rules/lock-file/index.js +1 -92
  10. package/src/rules/no-arbitrary-file-access/index.js +1 -152
  11. package/src/rules/no-buffer-overread/index.js +1 -543
  12. package/src/rules/no-cryptojs/index.js +1 -99
  13. package/src/rules/no-cryptojs-weak-random/index.js +1 -103
  14. package/src/rules/no-data-in-temp-storage/index.js +1 -85
  15. package/src/rules/no-deprecated-buffer/index.js +1 -84
  16. package/src/rules/no-deprecated-cipher-method/index.js +1 -112
  17. package/src/rules/no-dynamic-algorithm-selection/index.js +1 -72
  18. package/src/rules/no-dynamic-command-string/index.js +1 -201
  19. package/src/rules/no-dynamic-dependency-loading/index.js +1 -45
  20. package/src/rules/no-dynamic-require/index.js +1 -96
  21. package/src/rules/no-ecb-mode/index.js +1 -108
  22. package/src/rules/no-insecure-key-derivation/index.js +1 -109
  23. package/src/rules/no-insecure-rsa-padding/index.js +1 -104
  24. package/src/rules/no-math-random-crypto/index.js +1 -192
  25. package/src/rules/no-self-signed-certs/index.js +1 -110
  26. package/src/rules/no-sha1-hash/index.js +1 -121
  27. package/src/rules/no-shell-injection/index.js +1 -68
  28. package/src/rules/no-ssrf/index.js +1 -221
  29. package/src/rules/no-static-iv/index.js +1 -129
  30. package/src/rules/no-timing-unsafe-compare/index.js +1 -106
  31. package/src/rules/no-toctou-vulnerability/index.js +1 -195
  32. package/src/rules/no-unsafe-buffer-alloc/index.js +1 -87
  33. package/src/rules/no-unsafe-dynamic-require/index.js +1 -93
  34. package/src/rules/no-weak-cipher-algorithm/index.js +1 -174
  35. package/src/rules/no-weak-hash-algorithm/index.js +1 -199
  36. package/src/rules/no-zip-slip/index.js +1 -410
  37. package/src/rules/prefer-native-crypto/index.js +1 -119
  38. package/src/rules/require-dependency-integrity/index.js +1 -62
  39. package/src/rules/require-secure-credential-storage/index.js +1 -45
  40. package/src/rules/require-secure-deletion/index.js +1 -82
  41. package/src/rules/require-storage-encryption/index.js +1 -45
  42. package/src/types/index.js +1 -2
@@ -1,201 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.noDynamicCommandString = void 0;
4
- const eslint_devkit_1 = require("@interlace/eslint-devkit");
5
- const ARGV_FUNCTIONS = new Set([
6
- 'spawn',
7
- 'spawnSync',
8
- 'execFile',
9
- 'execFileSync',
10
- 'fork',
11
- ]);
12
- const POSIX_COMMAND_FLAGS = new Set(['-c']);
13
- const CMD_COMMAND_FLAGS = new Set(['/c', '/C', '/k', '/K']);
14
- const POWERSHELL_COMMAND_FLAGS = new Set([
15
- '-Command',
16
- '-command',
17
- '-c',
18
- '-EncodedCommand',
19
- '-encodedcommand',
20
- '-e',
21
- '-ec',
22
- ]);
23
- const SHELL_COMMAND_FLAGS = {
24
- sh: POSIX_COMMAND_FLAGS,
25
- bash: POSIX_COMMAND_FLAGS,
26
- zsh: POSIX_COMMAND_FLAGS,
27
- dash: POSIX_COMMAND_FLAGS,
28
- ksh: POSIX_COMMAND_FLAGS,
29
- busybox: POSIX_COMMAND_FLAGS,
30
- cmd: CMD_COMMAND_FLAGS,
31
- 'cmd.exe': CMD_COMMAND_FLAGS,
32
- powershell: POWERSHELL_COMMAND_FLAGS,
33
- 'powershell.exe': POWERSHELL_COMMAND_FLAGS,
34
- pwsh: POWERSHELL_COMMAND_FLAGS,
35
- };
36
- const COMMAND_RUNNERS = new Set([
37
- 'execaCommand',
38
- 'execaCommandSync',
39
- '$.raw',
40
- ]);
41
- function basename(command) {
42
- const segments = command.split(/[\\/]/);
43
- return segments[segments.length - 1];
44
- }
45
- function isAssembledString(node) {
46
- if (node.type === eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral) {
47
- return node.expressions.length > 0;
48
- }
49
- if (node.type === eslint_devkit_1.AST_NODE_TYPES.BinaryExpression) {
50
- return node.operator === '+';
51
- }
52
- return (node.type === eslint_devkit_1.AST_NODE_TYPES.Identifier ||
53
- node.type === eslint_devkit_1.AST_NODE_TYPES.MemberExpression ||
54
- node.type === eslint_devkit_1.AST_NODE_TYPES.CallExpression);
55
- }
56
- function calleeName(callee) {
57
- if (callee.type === eslint_devkit_1.AST_NODE_TYPES.Identifier)
58
- return callee.name;
59
- if (callee.type !== eslint_devkit_1.AST_NODE_TYPES.MemberExpression)
60
- return null;
61
- if (callee.property.type !== eslint_devkit_1.AST_NODE_TYPES.Identifier)
62
- return null;
63
- if (callee.computed)
64
- return null;
65
- if (callee.object.type === eslint_devkit_1.AST_NODE_TYPES.Identifier &&
66
- callee.object.name === '$') {
67
- return `$.${callee.property.name}`;
68
- }
69
- return callee.property.name;
70
- }
71
- exports.noDynamicCommandString = (0, eslint_devkit_1.createRule)({
72
- name: 'no-dynamic-command-string',
73
- meta: {
74
- type: 'problem',
75
- docs: {
76
- url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-dynamic-command-string.md',
77
- description: 'Disallow dynamically assembled command strings passed to a shell flag or to a command-runner that does not escape (CWE-77)',
78
- cwe: 'CWE-77',
79
- cvss: 9.8,
80
- confidence: 'high',
81
- },
82
- messages: {
83
- shellFlagInjection: (0, eslint_devkit_1.formatLLMMessage)({
84
- icon: eslint_devkit_1.MessageIcons.SECURITY,
85
- issueName: 'Command Injection Through Shell Flag (CWE-77)',
86
- cwe: 'CWE-77',
87
- cvss: 9.8,
88
- description: '{{fn}}("{{shell}}", ["{{flag}}", …]) hands a dynamically built string to {{shell}}, which parses it as a command line. The argument array looks parameterized but everything after {{flag}} is re-parsed — `;`, `&&`, backticks and `$()` all execute.',
89
- severity: 'CRITICAL',
90
- fix: 'Invoke the target program directly with its own argument array — spawn("kill", [String(pid)]) — instead of routing it through a shell.',
91
- documentationLink: 'https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html',
92
- }),
93
- commandStringInterpolation: (0, eslint_devkit_1.formatLLMMessage)({
94
- icon: eslint_devkit_1.MessageIcons.SECURITY,
95
- issueName: 'Interpolated Command Line (CWE-77)',
96
- cwe: 'CWE-77',
97
- cvss: 9.8,
98
- description: '{{fn}}() takes a whole command line and does NOT escape interpolated values (unlike the execa/zx tagged-template forms). Any special character in the interpolated value changes which command runs.',
99
- severity: 'CRITICAL',
100
- fix: 'Use the tagged-template or array form that escapes for you: execa("git", ["clone", url]) or await $`git clone ${url}`.',
101
- documentationLink: 'https://cwe.mitre.org/data/definitions/77.html',
102
- }),
103
- },
104
- schema: [
105
- {
106
- type: 'object',
107
- properties: {
108
- extraCommandRunners: {
109
- type: 'array',
110
- items: { type: 'string' },
111
- description: 'Extra functions that accept a full command line without escaping',
112
- },
113
- },
114
- additionalProperties: false,
115
- },
116
- ],
117
- },
118
- defaultOptions: [{}],
119
- create(context, [options]) {
120
- const { extraCommandRunners } = options;
121
- const runners = new Set([
122
- ...COMMAND_RUNNERS,
123
- ...(extraCommandRunners ?? []),
124
- ]);
125
- function checkShellFlag(node, fn) {
126
- const command = node.arguments[0];
127
- if (!command ||
128
- command.type !== eslint_devkit_1.AST_NODE_TYPES.Literal ||
129
- typeof command.value !== 'string') {
130
- return;
131
- }
132
- const shell = basename(command.value);
133
- const commandFlags = SHELL_COMMAND_FLAGS[shell.toLowerCase()];
134
- if (!commandFlags)
135
- return;
136
- const argv = node.arguments[1];
137
- if (!argv || argv.type !== eslint_devkit_1.AST_NODE_TYPES.ArrayExpression)
138
- return;
139
- for (let i = 0; i < argv.elements.length - 1; i += 1) {
140
- const flag = argv.elements[i];
141
- if (!flag || flag.type !== eslint_devkit_1.AST_NODE_TYPES.Literal)
142
- continue;
143
- if (typeof flag.value !== 'string')
144
- continue;
145
- if (!commandFlags.has(flag.value))
146
- continue;
147
- const commandString = argv.elements[i + 1];
148
- if (!commandString)
149
- continue;
150
- if (!isAssembledString(commandString))
151
- continue;
152
- context.report({
153
- node: commandString,
154
- messageId: 'shellFlagInjection',
155
- data: { fn, shell, flag: flag.value },
156
- });
157
- return;
158
- }
159
- }
160
- function checkCommandRunner(node, fn) {
161
- const commandLine = node.arguments[0];
162
- if (!commandLine)
163
- return;
164
- if (commandLine.type === eslint_devkit_1.AST_NODE_TYPES.Literal)
165
- return;
166
- if (!isAssembledString(commandLine))
167
- return;
168
- context.report({
169
- node: commandLine,
170
- messageId: 'commandStringInterpolation',
171
- data: { fn },
172
- });
173
- }
174
- return {
175
- CallExpression(node) {
176
- const fn = calleeName(node.callee);
177
- if (!fn)
178
- return;
179
- if (ARGV_FUNCTIONS.has(fn)) {
180
- checkShellFlag(node, fn);
181
- return;
182
- }
183
- if (runners.has(fn)) {
184
- checkCommandRunner(node, fn);
185
- }
186
- },
187
- TaggedTemplateExpression(node) {
188
- const fn = calleeName(node.tag);
189
- if (!fn || !runners.has(fn))
190
- return;
191
- if (node.quasi.expressions.length === 0)
192
- return;
193
- context.report({
194
- node: node.quasi,
195
- messageId: 'commandStringInterpolation',
196
- data: { fn },
197
- });
198
- },
199
- };
200
- },
201
- });
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noDynamicCommandString=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const ARGV_FUNCTIONS=new Set(["spawn","spawnSync","execFile","execFileSync","fork"]);const POSIX_COMMAND_FLAGS=new Set(["-c"]);const CMD_COMMAND_FLAGS=new Set(["/c","/C","/k","/K"]);const POWERSHELL_COMMAND_FLAGS=new Set(["-Command","-command","-c","-EncodedCommand","-encodedcommand","-e","-ec"]);const SHELL_COMMAND_FLAGS={sh:POSIX_COMMAND_FLAGS,bash:POSIX_COMMAND_FLAGS,zsh:POSIX_COMMAND_FLAGS,dash:POSIX_COMMAND_FLAGS,ksh:POSIX_COMMAND_FLAGS,busybox:POSIX_COMMAND_FLAGS,cmd:CMD_COMMAND_FLAGS,"cmd.exe":CMD_COMMAND_FLAGS,powershell:POWERSHELL_COMMAND_FLAGS,"powershell.exe":POWERSHELL_COMMAND_FLAGS,pwsh:POWERSHELL_COMMAND_FLAGS};const COMMAND_RUNNERS=new Set(["execaCommand","execaCommandSync","$.raw"]);function basename(command){const segments=command.split(/[\\/]/);return segments[segments.length-1]}function isAssembledString(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral){return node.expressions.length>0}if(node.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression){return node.operator==="+"}return node.type===eslint_devkit_1.AST_NODE_TYPES.Identifier||node.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression||node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression}function calleeName(callee){if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return callee.name;if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return null;if(callee.property.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return null;if(callee.computed)return null;if(callee.object.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&callee.object.name==="$"){return`$.${callee.property.name}`}return callee.property.name}exports.noDynamicCommandString=(0,eslint_devkit_1.createRule)({name:"no-dynamic-command-string",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-dynamic-command-string.md",description:"Disallow dynamically assembled command strings passed to a shell flag or to a command-runner that does not escape (CWE-77)",cwe:"CWE-77",cvss:9.8,confidence:"high"},messages:{shellFlagInjection:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Command Injection Through Shell Flag (CWE-77)",cwe:"CWE-77",cvss:9.8,description:'{{fn}}("{{shell}}", ["{{flag}}", \u2026]) hands a dynamically built string to {{shell}}, which parses it as a command line. The argument array looks parameterized but everything after {{flag}} is re-parsed \u2014 `;`, `&&`, backticks and `$()` all execute.',severity:"CRITICAL",fix:'Invoke the target program directly with its own argument array \u2014 spawn("kill", [String(pid)]) \u2014 instead of routing it through a shell.',documentationLink:"https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html"}),commandStringInterpolation:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Interpolated Command Line (CWE-77)",cwe:"CWE-77",cvss:9.8,description:"{{fn}}() takes a whole command line and does NOT escape interpolated values (unlike the execa/zx tagged-template forms). Any special character in the interpolated value changes which command runs.",severity:"CRITICAL",fix:'Use the tagged-template or array form that escapes for you: execa("git", ["clone", url]) or await $`git clone ${url}`.',documentationLink:"https://cwe.mitre.org/data/definitions/77.html"})},schema:[{type:"object",properties:{extraCommandRunners:{type:"array",items:{type:"string"},description:"Extra functions that accept a full command line without escaping"}},additionalProperties:false}]},defaultOptions:[{}],create(context,[options]){const{extraCommandRunners}=options;const runners=new Set([...COMMAND_RUNNERS,...extraCommandRunners??[]]);function checkShellFlag(node,fn){const command=node.arguments[0];if(!command||command.type!==eslint_devkit_1.AST_NODE_TYPES.Literal||typeof command.value!=="string"){return}const shell=basename(command.value);const commandFlags=SHELL_COMMAND_FLAGS[shell.toLowerCase()];if(!commandFlags)return;const argv=node.arguments[1];if(!argv||argv.type!==eslint_devkit_1.AST_NODE_TYPES.ArrayExpression)return;for(let i=0;i<argv.elements.length-1;i+=1){const flag=argv.elements[i];if(!flag||flag.type!==eslint_devkit_1.AST_NODE_TYPES.Literal)continue;if(typeof flag.value!=="string")continue;if(!commandFlags.has(flag.value))continue;const commandString=argv.elements[i+1];if(!commandString)continue;if(!isAssembledString(commandString))continue;context.report({node:commandString,messageId:"shellFlagInjection",data:{fn,shell,flag:flag.value}});return}}function checkCommandRunner(node,fn){const commandLine=node.arguments[0];if(!commandLine)return;if(commandLine.type===eslint_devkit_1.AST_NODE_TYPES.Literal)return;if(!isAssembledString(commandLine))return;context.report({node:commandLine,messageId:"commandStringInterpolation",data:{fn}})}return{CallExpression(node){const fn=calleeName(node.callee);if(!fn)return;if(ARGV_FUNCTIONS.has(fn)){checkShellFlag(node,fn);return}if(runners.has(fn)){checkCommandRunner(node,fn)}},TaggedTemplateExpression(node){const fn=calleeName(node.tag);if(!fn||!runners.has(fn))return;if(node.quasi.expressions.length===0)return;context.report({node:node.quasi,messageId:"commandStringInterpolation",data:{fn}})}}}});
@@ -1,45 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.noDynamicDependencyLoading = void 0;
4
- const eslint_devkit_1 = require("@interlace/eslint-devkit");
5
- exports.noDynamicDependencyLoading = (0, eslint_devkit_1.createRule)({
6
- name: 'no-dynamic-dependency-loading',
7
- meta: {
8
- type: 'problem',
9
- docs: {
10
- url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-dynamic-dependency-loading.md',
11
- description: 'Prevent runtime dependency injection with dynamic paths',
12
- cwe: 'CWE-1104',
13
- cvss: 5.3,
14
- },
15
- messages: {
16
- violationDetected: (0, eslint_devkit_1.formatLLMMessage)({
17
- icon: eslint_devkit_1.MessageIcons.SECURITY,
18
- issueName: 'violation Detected',
19
- cwe: 'CWE-1104',
20
- description: 'Dynamic import/require detected - use static imports for security',
21
- severity: 'HIGH',
22
- fix: 'Review and apply secure practices',
23
- documentationLink: 'https://cwe.mitre.org/data/definitions/1104.html',
24
- })
25
- },
26
- schema: [],
27
- },
28
- defaultOptions: [],
29
- create(context) {
30
- return {
31
- CallExpression(node) {
32
- if (node.callee.type === eslint_devkit_1.AST_NODE_TYPES.Identifier &&
33
- node.callee.name === 'require' &&
34
- node.arguments[0]?.type !== eslint_devkit_1.AST_NODE_TYPES.Literal) {
35
- context.report({ node, messageId: 'violationDetected' });
36
- }
37
- },
38
- ImportExpression(node) {
39
- if (node.source.type !== 'Literal') {
40
- context.report({ node, messageId: 'violationDetected' });
41
- }
42
- },
43
- };
44
- },
45
- });
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noDynamicDependencyLoading=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.noDynamicDependencyLoading=(0,eslint_devkit_1.createRule)({name:"no-dynamic-dependency-loading",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-dynamic-dependency-loading.md",description:"Prevent runtime dependency injection with dynamic paths",cwe:"CWE-1104",cvss:5.3},messages:{violationDetected:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"violation Detected",cwe:"CWE-1104",description:"Dynamic import/require detected - use static imports for security",severity:"HIGH",fix:"Review and apply secure practices",documentationLink:"https://cwe.mitre.org/data/definitions/1104.html"})},schema:[]},defaultOptions:[],create(context){return{CallExpression(node){if(node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.name==="require"&&node.arguments[0]?.type!==eslint_devkit_1.AST_NODE_TYPES.Literal){context.report({node,messageId:"violationDetected"})}},ImportExpression(node){if(node.source.type!=="Literal"){context.report({node,messageId:"violationDetected"})}}}}});
@@ -1,96 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.noDynamicRequire = void 0;
4
- const eslint_devkit_1 = require("@interlace/eslint-devkit");
5
- const eslint_devkit_2 = require("@interlace/eslint-devkit");
6
- exports.noDynamicRequire = (0, eslint_devkit_1.createRule)({
7
- name: 'no-dynamic-require',
8
- meta: {
9
- type: 'problem',
10
- docs: {
11
- url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-dynamic-require.md',
12
- description: 'Forbid `require()` calls with expressions',
13
- cwe: 'CWE-94',
14
- cweJustification: 'CWE-94 (Improper Control of Generation of Code) — dynamic require with attacker-influenced path can load arbitrary modules, equivalent to remote code execution.',
15
- confidence: 'high',
16
- },
17
- hasSuggestions: false,
18
- messages: {
19
- dynamicRequire: (0, eslint_devkit_2.formatLLMMessage)({
20
- icon: eslint_devkit_2.MessageIcons.WARNING,
21
- issueName: 'Dynamic Require',
22
- description: 'Require call uses dynamic expression',
23
- severity: 'HIGH',
24
- fix: 'Use static string literals for require() calls',
25
- documentationLink: 'https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-dynamic-require.md',
26
- }),
27
- },
28
- schema: [
29
- {
30
- type: 'object',
31
- properties: {
32
- allowContexts: {
33
- type: 'array',
34
- items: {
35
- type: 'string',
36
- enum: ['test', 'config', 'build', 'runtime'],
37
- },
38
- description: 'Allow dynamic requires in specific contexts.',
39
- },
40
- allowPatterns: {
41
- type: 'array',
42
- items: { type: 'string' },
43
- description: 'Regex patterns for allowed dynamic require paths.',
44
- },
45
- },
46
- additionalProperties: false,
47
- },
48
- ],
49
- },
50
- defaultOptions: [{
51
- allowContexts: [],
52
- allowPatterns: []
53
- }],
54
- create(context) {
55
- const [options] = context.options;
56
- const { allowContexts = [], } = options || {};
57
- const filename = context.filename || '';
58
- function isInAllowedContext() {
59
- if (allowContexts.includes('test') && (filename.includes('.test.') || filename.includes('.spec.') || filename.includes('/__tests__/'))) {
60
- return true;
61
- }
62
- if (allowContexts.includes('config') && (filename.includes('config') || filename.includes('webpack') || filename.includes('rollup'))) {
63
- return true;
64
- }
65
- if (allowContexts.includes('build') && (filename.includes('build') || filename.includes('scripts') || filename.includes('tools'))) {
66
- return true;
67
- }
68
- if (allowContexts.includes('runtime') && (filename.includes('runtime') || filename.includes('dynamic'))) {
69
- return true;
70
- }
71
- return false;
72
- }
73
- function isStaticLiteral(node) {
74
- return node.type === 'Literal' && typeof node.value === 'string';
75
- }
76
- return {
77
- CallExpression(node) {
78
- if (node.callee.type === 'Identifier' &&
79
- node.callee.name === 'require' &&
80
- node.arguments.length === 1) {
81
- const requireArg = node.arguments[0];
82
- if (isInAllowedContext()) {
83
- return;
84
- }
85
- if (isStaticLiteral(requireArg)) {
86
- return;
87
- }
88
- context.report({
89
- node: requireArg,
90
- messageId: 'dynamicRequire',
91
- });
92
- }
93
- },
94
- };
95
- },
96
- });
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noDynamicRequire=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const eslint_devkit_2=require("@interlace/eslint-devkit");exports.noDynamicRequire=(0,eslint_devkit_1.createRule)({name:"no-dynamic-require",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-dynamic-require.md",description:"Forbid `require()` calls with expressions",cwe:"CWE-94",cweJustification:"CWE-94 (Improper Control of Generation of Code) \u2014 dynamic require with attacker-influenced path can load arbitrary modules, equivalent to remote code execution.",confidence:"high"},hasSuggestions:false,messages:{dynamicRequire:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.WARNING,issueName:"Dynamic Require",description:"Require call uses dynamic expression",severity:"HIGH",fix:"Use static string literals for require() calls",documentationLink:"https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-dynamic-require.md"})},schema:[{type:"object",properties:{allowContexts:{type:"array",items:{type:"string",enum:["test","config","build","runtime"]},description:"Allow dynamic requires in specific contexts."},allowPatterns:{type:"array",items:{type:"string"},description:"Regex patterns for allowed dynamic require paths."}},additionalProperties:false}]},defaultOptions:[{allowContexts:[],allowPatterns:[]}],create(context){const[options]=context.options;const{allowContexts=[]}=options||{};const filename=context.filename||"";function isInAllowedContext(){if(allowContexts.includes("test")&&(filename.includes(".test.")||filename.includes(".spec.")||filename.includes("/__tests__/"))){return true}if(allowContexts.includes("config")&&(filename.includes("config")||filename.includes("webpack")||filename.includes("rollup"))){return true}if(allowContexts.includes("build")&&(filename.includes("build")||filename.includes("scripts")||filename.includes("tools"))){return true}if(allowContexts.includes("runtime")&&(filename.includes("runtime")||filename.includes("dynamic"))){return true}return false}function isStaticLiteral(node){return node.type==="Literal"&&typeof node.value==="string"}return{CallExpression(node){if(node.callee.type==="Identifier"&&node.callee.name==="require"&&node.arguments.length===1){const requireArg=node.arguments[0];if(isInAllowedContext()){return}if(isStaticLiteral(requireArg)){return}context.report({node:requireArg,messageId:"dynamicRequire"})}}}}});
@@ -1,108 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.noEcbMode = void 0;
4
- const eslint_devkit_1 = require("@interlace/eslint-devkit");
5
- exports.noEcbMode = (0, eslint_devkit_1.createRule)({
6
- name: 'no-ecb-mode',
7
- meta: {
8
- type: 'problem',
9
- docs: {
10
- url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-ecb-mode.md',
11
- description: 'Disallow ECB encryption mode (use GCM or CBC instead)',
12
- cwe: 'CWE-327',
13
- cvss: 7.5,
14
- },
15
- hasSuggestions: true,
16
- messages: {
17
- ecbMode: (0, eslint_devkit_1.formatLLMMessage)({
18
- icon: eslint_devkit_1.MessageIcons.SECURITY,
19
- issueName: 'ECB mode detected',
20
- cwe: 'CWE-327',
21
- description: 'ECB mode encrypts identical plaintext blocks to identical ciphertext, leaking data patterns. Famous example: the "ECB penguin".',
22
- severity: 'HIGH',
23
- fix: 'Use GCM mode for authenticated encryption: crypto.createCipheriv("aes-256-gcm", key, iv)',
24
- documentationLink: 'https://blog.cloudflare.com/why-are-some-images-more-secure-than-others/',
25
- }),
26
- useGcm: (0, eslint_devkit_1.formatLLMMessage)({
27
- icon: eslint_devkit_1.MessageIcons.INFO,
28
- issueName: 'Use GCM mode',
29
- description: 'GCM provides authenticated encryption (confidentiality + integrity)',
30
- severity: 'LOW',
31
- fix: 'crypto.createCipheriv("aes-256-gcm", key, iv)',
32
- documentationLink: 'https://nodejs.org/api/crypto.html#cryptocreatecipherivalgorithm-key-iv-options',
33
- }),
34
- useCbc: (0, eslint_devkit_1.formatLLMMessage)({
35
- icon: eslint_devkit_1.MessageIcons.INFO,
36
- issueName: 'Use CBC mode',
37
- description: 'CBC with HMAC provides confidentiality (add separate MAC for integrity)',
38
- severity: 'LOW',
39
- fix: 'crypto.createCipheriv("aes-256-cbc", key, iv)',
40
- documentationLink: 'https://nodejs.org/api/crypto.html#cryptocreatecipherivalgorithm-key-iv-options',
41
- }),
42
- },
43
- schema: [
44
- {
45
- type: 'object',
46
- properties: {
47
- allowInTests: {
48
- type: 'boolean',
49
- default: false,
50
- description: 'Allow ECB mode in test files',
51
- },
52
- },
53
- additionalProperties: false,
54
- },
55
- ],
56
- },
57
- defaultOptions: [
58
- {
59
- allowInTests: false,
60
- },
61
- ],
62
- create(context, [options = {}]) {
63
- const { allowInTests = false } = options;
64
- const filename = context.filename;
65
- const isTestFile = allowInTests && /\.(test|spec)\.(ts|tsx|js|jsx)$/.test(filename);
66
- function checkCallExpression(node) {
67
- if (isTestFile)
68
- return;
69
- const cipherMethods = new Set(['createCipher', 'createCipheriv', 'createDecipher', 'createDecipheriv']);
70
- const isCipherCall = (node.callee.type === eslint_devkit_1.AST_NODE_TYPES.MemberExpression &&
71
- node.callee.property.type === eslint_devkit_1.AST_NODE_TYPES.Identifier &&
72
- cipherMethods.has(node.callee.property.name)) ||
73
- (node.callee.type === eslint_devkit_1.AST_NODE_TYPES.Identifier &&
74
- cipherMethods.has(node.callee.name));
75
- if (isCipherCall && node.arguments.length >= 1) {
76
- const algorithmArg = node.arguments[0];
77
- if (algorithmArg.type === eslint_devkit_1.AST_NODE_TYPES.Literal && typeof algorithmArg.value === 'string') {
78
- const algorithm = algorithmArg.value.toLowerCase();
79
- if (algorithm.includes('-ecb') || algorithm.endsWith('ecb')) {
80
- const gcmReplacement = algorithm.replace(/-?ecb$/, '-gcm');
81
- context.report({
82
- node: algorithmArg,
83
- messageId: 'ecbMode',
84
- suggest: [
85
- {
86
- messageId: 'useGcm',
87
- fix: (fixer) => {
88
- return fixer.replaceText(algorithmArg, `"${gcmReplacement}"`);
89
- },
90
- },
91
- {
92
- messageId: 'useCbc',
93
- fix: (fixer) => {
94
- const cbcReplacement = algorithm.replace(/-?ecb$/, '-cbc');
95
- return fixer.replaceText(algorithmArg, `"${cbcReplacement}"`);
96
- },
97
- },
98
- ],
99
- });
100
- }
101
- }
102
- }
103
- }
104
- return {
105
- CallExpression: checkCallExpression,
106
- };
107
- },
108
- });
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noEcbMode=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.noEcbMode=(0,eslint_devkit_1.createRule)({name:"no-ecb-mode",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-ecb-mode.md",description:"Disallow ECB encryption mode (use GCM or CBC instead)",cwe:"CWE-327",cvss:7.5},hasSuggestions:true,messages:{ecbMode:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"ECB mode detected",cwe:"CWE-327",description:'ECB mode encrypts identical plaintext blocks to identical ciphertext, leaking data patterns. Famous example: the "ECB penguin".',severity:"HIGH",fix:'Use GCM mode for authenticated encryption: crypto.createCipheriv("aes-256-gcm", key, iv)',documentationLink:"https://blog.cloudflare.com/why-are-some-images-more-secure-than-others/"}),useGcm:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use GCM mode",description:"GCM provides authenticated encryption (confidentiality + integrity)",severity:"LOW",fix:'crypto.createCipheriv("aes-256-gcm", key, iv)',documentationLink:"https://nodejs.org/api/crypto.html#cryptocreatecipherivalgorithm-key-iv-options"}),useCbc:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use CBC mode",description:"CBC with HMAC provides confidentiality (add separate MAC for integrity)",severity:"LOW",fix:'crypto.createCipheriv("aes-256-cbc", key, iv)',documentationLink:"https://nodejs.org/api/crypto.html#cryptocreatecipherivalgorithm-key-iv-options"})},schema:[{type:"object",properties:{allowInTests:{type:"boolean",default:false,description:"Allow ECB mode in test files"}},additionalProperties:false}]},defaultOptions:[{allowInTests:false}],create(context,[options={}]){const{allowInTests=false}=options;const filename=context.filename;const isTestFile=allowInTests&&/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(filename);function checkCallExpression(node){if(isTestFile)return;const cipherMethods=new Set(["createCipher","createCipheriv","createDecipher","createDecipheriv"]);const isCipherCall=node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&cipherMethods.has(node.callee.property.name)||node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&cipherMethods.has(node.callee.name);if(isCipherCall&&node.arguments.length>=1){const algorithmArg=node.arguments[0];if(algorithmArg.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof algorithmArg.value==="string"){const algorithm=algorithmArg.value.toLowerCase();if(algorithm.includes("-ecb")||algorithm.endsWith("ecb")){const gcmReplacement=algorithm.replace(/-?ecb$/,"-gcm");context.report({node:algorithmArg,messageId:"ecbMode",suggest:[{messageId:"useGcm",fix:fixer=>{return fixer.replaceText(algorithmArg,`"${gcmReplacement}"`)}},{messageId:"useCbc",fix:fixer=>{const cbcReplacement=algorithm.replace(/-?ecb$/,"-cbc");return fixer.replaceText(algorithmArg,`"${cbcReplacement}"`)}}]})}}}}return{CallExpression:checkCallExpression}}});
@@ -1,109 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.noInsecureKeyDerivation = void 0;
4
- const eslint_devkit_1 = require("@interlace/eslint-devkit");
5
- const DEFAULT_MIN_ITERATIONS = 100000;
6
- exports.noInsecureKeyDerivation = (0, eslint_devkit_1.createRule)({
7
- name: 'no-insecure-key-derivation',
8
- meta: {
9
- type: 'problem',
10
- docs: {
11
- url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-insecure-key-derivation.md',
12
- description: 'Disallow PBKDF2 with insufficient iterations (< 100,000)',
13
- cwe: 'CWE-916',
14
- cvss: 7.5,
15
- },
16
- hasSuggestions: true,
17
- messages: {
18
- insufficientIterations: (0, eslint_devkit_1.formatLLMMessage)({
19
- icon: eslint_devkit_1.MessageIcons.SECURITY,
20
- issueName: 'Insufficient PBKDF2 iterations',
21
- cwe: 'CWE-916',
22
- description: 'PBKDF2 with {{actual}} iterations is too low. Minimum recommended: {{minimum}} iterations (OWASP 2023).',
23
- severity: 'HIGH',
24
- fix: 'Increase iterations to at least {{minimum}}, or use scrypt/Argon2',
25
- documentationLink: 'https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html',
26
- }),
27
- useMinIterations: (0, eslint_devkit_1.formatLLMMessage)({
28
- icon: eslint_devkit_1.MessageIcons.INFO,
29
- issueName: 'Use minimum iterations',
30
- description: 'Use at least {{minimum}} iterations for PBKDF2',
31
- severity: 'LOW',
32
- fix: 'crypto.pbkdf2(password, salt, {{minimum}}, keylen, digest)',
33
- documentationLink: 'https://nodejs.org/api/crypto.html#cryptopbkdf2password-salt-iterations-keylen-digest-callback',
34
- }),
35
- useScrypt: (0, eslint_devkit_1.formatLLMMessage)({
36
- icon: eslint_devkit_1.MessageIcons.INFO,
37
- issueName: 'Use scrypt',
38
- description: 'scrypt is memory-hard and resistant to GPU/ASIC attacks',
39
- severity: 'LOW',
40
- fix: 'crypto.scrypt(password, salt, keylen)',
41
- documentationLink: 'https://nodejs.org/api/crypto.html#cryptoscryptpassword-salt-keylen-options-callback',
42
- }),
43
- useArgon2: (0, eslint_devkit_1.formatLLMMessage)({
44
- icon: eslint_devkit_1.MessageIcons.INFO,
45
- issueName: 'Use Argon2',
46
- description: 'Argon2id is the winner of the Password Hashing Competition',
47
- severity: 'LOW',
48
- fix: 'argon2.hash(password, { type: argon2.argon2id })',
49
- documentationLink: 'https://github.com/ranisalt/node-argon2',
50
- }),
51
- },
52
- schema: [
53
- {
54
- type: 'object',
55
- properties: {
56
- minIterations: {
57
- type: 'number',
58
- default: DEFAULT_MIN_ITERATIONS,
59
- description: 'Minimum required PBKDF2 iterations',
60
- },
61
- },
62
- additionalProperties: false,
63
- },
64
- ],
65
- },
66
- defaultOptions: [
67
- {
68
- minIterations: DEFAULT_MIN_ITERATIONS,
69
- },
70
- ],
71
- create(context, [options = {}]) {
72
- const { minIterations = DEFAULT_MIN_ITERATIONS } = options;
73
- function checkCallExpression(node) {
74
- const isPbkdf2Call = (node.callee.type === eslint_devkit_1.AST_NODE_TYPES.MemberExpression &&
75
- node.callee.property.type === eslint_devkit_1.AST_NODE_TYPES.Identifier &&
76
- (node.callee.property.name === 'pbkdf2' || node.callee.property.name === 'pbkdf2Sync')) ||
77
- (node.callee.type === eslint_devkit_1.AST_NODE_TYPES.Identifier &&
78
- (node.callee.name === 'pbkdf2' || node.callee.name === 'pbkdf2Sync'));
79
- if (isPbkdf2Call) {
80
- const iterationsArg = node.arguments[2];
81
- if (iterationsArg?.type === eslint_devkit_1.AST_NODE_TYPES.Literal && typeof iterationsArg.value === 'number') {
82
- const iterations = iterationsArg.value;
83
- if (iterations < minIterations) {
84
- context.report({
85
- node: iterationsArg,
86
- messageId: 'insufficientIterations',
87
- data: {
88
- actual: String(iterations),
89
- minimum: String(minIterations),
90
- },
91
- suggest: [
92
- {
93
- messageId: 'useMinIterations',
94
- data: { minimum: String(minIterations) },
95
- fix: (fixer) => {
96
- return fixer.replaceText(iterationsArg, String(minIterations));
97
- },
98
- },
99
- ],
100
- });
101
- }
102
- }
103
- }
104
- }
105
- return {
106
- CallExpression: checkCallExpression,
107
- };
108
- },
109
- });
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noInsecureKeyDerivation=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const DEFAULT_MIN_ITERATIONS=1e5;exports.noInsecureKeyDerivation=(0,eslint_devkit_1.createRule)({name:"no-insecure-key-derivation",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/no-insecure-key-derivation.md",description:"Disallow PBKDF2 with insufficient iterations (< 100,000)",cwe:"CWE-916",cvss:7.5},hasSuggestions:true,messages:{insufficientIterations:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Insufficient PBKDF2 iterations",cwe:"CWE-916",description:"PBKDF2 with {{actual}} iterations is too low. Minimum recommended: {{minimum}} iterations (OWASP 2023).",severity:"HIGH",fix:"Increase iterations to at least {{minimum}}, or use scrypt/Argon2",documentationLink:"https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html"}),useMinIterations:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use minimum iterations",description:"Use at least {{minimum}} iterations for PBKDF2",severity:"LOW",fix:"crypto.pbkdf2(password, salt, {{minimum}}, keylen, digest)",documentationLink:"https://nodejs.org/api/crypto.html#cryptopbkdf2password-salt-iterations-keylen-digest-callback"}),useScrypt:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use scrypt",description:"scrypt is memory-hard and resistant to GPU/ASIC attacks",severity:"LOW",fix:"crypto.scrypt(password, salt, keylen)",documentationLink:"https://nodejs.org/api/crypto.html#cryptoscryptpassword-salt-keylen-options-callback"}),useArgon2:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use Argon2",description:"Argon2id is the winner of the Password Hashing Competition",severity:"LOW",fix:"argon2.hash(password, { type: argon2.argon2id })",documentationLink:"https://github.com/ranisalt/node-argon2"})},schema:[{type:"object",properties:{minIterations:{type:"number",default:DEFAULT_MIN_ITERATIONS,description:"Minimum required PBKDF2 iterations"}},additionalProperties:false}]},defaultOptions:[{minIterations:DEFAULT_MIN_ITERATIONS}],create(context,[options={}]){const{minIterations=DEFAULT_MIN_ITERATIONS}=options;function checkCallExpression(node){const isPbkdf2Call=node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&node.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&(node.callee.property.name==="pbkdf2"||node.callee.property.name==="pbkdf2Sync")||node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&(node.callee.name==="pbkdf2"||node.callee.name==="pbkdf2Sync");if(isPbkdf2Call){const iterationsArg=node.arguments[2];if(iterationsArg?.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&typeof iterationsArg.value==="number"){const iterations=iterationsArg.value;if(iterations<minIterations){context.report({node:iterationsArg,messageId:"insufficientIterations",data:{actual:String(iterations),minimum:String(minIterations)},suggest:[{messageId:"useMinIterations",data:{minimum:String(minIterations)},fix:fixer=>{return fixer.replaceText(iterationsArg,String(minIterations))}}]})}}}}return{CallExpression:checkCallExpression}}});