eslint-plugin-node-security 4.7.3 → 4.8.1
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.
- package/package.json +2 -2
- package/src/index.js +1 -95
- package/src/oxlint.js +1 -3
- package/src/rules/detect-child-process/index.js +1 -625
- package/src/rules/detect-eval-with-expression/index.js +1 -364
- package/src/rules/detect-non-literal-fs-filename/index.js +1 -431
- package/src/rules/detect-suspicious-dependencies/index.js +1 -69
- package/src/rules/lock-file/index.js +1 -92
- package/src/rules/no-arbitrary-file-access/index.js +1 -152
- package/src/rules/no-buffer-overread/index.js +1 -543
- package/src/rules/no-cryptojs/index.js +1 -99
- package/src/rules/no-cryptojs-weak-random/index.js +1 -103
- package/src/rules/no-data-in-temp-storage/index.js +1 -85
- package/src/rules/no-deprecated-buffer/index.js +1 -84
- package/src/rules/no-deprecated-cipher-method/index.js +1 -112
- package/src/rules/no-dynamic-algorithm-selection/index.js +1 -72
- package/src/rules/no-dynamic-command-string/index.js +1 -201
- package/src/rules/no-dynamic-dependency-loading/index.js +1 -45
- package/src/rules/no-dynamic-require/index.js +1 -96
- package/src/rules/no-ecb-mode/index.js +1 -108
- package/src/rules/no-insecure-key-derivation/index.js +1 -109
- package/src/rules/no-insecure-rsa-padding/index.js +1 -104
- package/src/rules/no-math-random-crypto/index.js +1 -192
- package/src/rules/no-self-signed-certs/index.js +1 -110
- package/src/rules/no-sha1-hash/index.js +1 -121
- package/src/rules/no-shell-injection/index.js +1 -68
- package/src/rules/no-ssrf/index.js +1 -221
- package/src/rules/no-static-iv/index.js +1 -129
- package/src/rules/no-timing-unsafe-compare/index.js +1 -106
- package/src/rules/no-toctou-vulnerability/index.js +1 -195
- package/src/rules/no-unsafe-buffer-alloc/index.js +1 -87
- package/src/rules/no-unsafe-dynamic-require/index.js +1 -93
- package/src/rules/no-weak-cipher-algorithm/index.js +1 -174
- package/src/rules/no-weak-hash-algorithm/index.js +1 -199
- package/src/rules/no-zip-slip/index.js +1 -410
- package/src/rules/prefer-native-crypto/index.js +1 -119
- package/src/rules/require-dependency-integrity/index.js +1 -62
- package/src/rules/require-secure-credential-storage/index.js +1 -45
- package/src/rules/require-secure-deletion/index.js +1 -82
- package/src/rules/require-storage-encryption/index.js +1 -45
- package/src/types/index.js +1 -2
|
@@ -1,625 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.detectChildProcess = exports.generateRefactoringSteps = void 0;
|
|
4
|
-
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
5
|
-
const eslint_devkit_2 = require("@interlace/eslint-devkit");
|
|
6
|
-
const COMMAND_PATTERNS = [
|
|
7
|
-
{
|
|
8
|
-
method: 'exec',
|
|
9
|
-
dangerous: true,
|
|
10
|
-
vulnerability: 'command-injection',
|
|
11
|
-
safeAlternatives: ['execFile', 'spawn'],
|
|
12
|
-
example: {
|
|
13
|
-
bad: 'exec(`git clone ${repoUrl}`)',
|
|
14
|
-
good: [
|
|
15
|
-
'execFile(\'git\', [\'clone\', repoUrl], {shell: false})',
|
|
16
|
-
'spawn(\'git\', [\'clone\', repoUrl], {shell: false})'
|
|
17
|
-
]
|
|
18
|
-
},
|
|
19
|
-
effort: '15-25 minutes'
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
method: 'execSync',
|
|
23
|
-
dangerous: true,
|
|
24
|
-
vulnerability: 'command-injection',
|
|
25
|
-
safeAlternatives: ['execFileSync', 'spawnSync'],
|
|
26
|
-
example: {
|
|
27
|
-
bad: 'execSync(`npm install ${packageName}`)',
|
|
28
|
-
good: [
|
|
29
|
-
'execFileSync(\'npm\', [\'install\', packageName], {shell: false})',
|
|
30
|
-
'spawnSync(\'npm\', [\'install\', packageName], {shell: false})'
|
|
31
|
-
]
|
|
32
|
-
},
|
|
33
|
-
effort: '15-25 minutes'
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
method: 'spawn',
|
|
37
|
-
dangerous: false,
|
|
38
|
-
vulnerability: 'argument-injection',
|
|
39
|
-
safeAlternatives: ['spawn with validation'],
|
|
40
|
-
example: {
|
|
41
|
-
bad: 'spawn(\'bash\', [\'-c\', userCommand])',
|
|
42
|
-
good: [
|
|
43
|
-
'spawn(validatedCommand, validatedArgs, {shell: false})',
|
|
44
|
-
'// Validate command and args first'
|
|
45
|
-
]
|
|
46
|
-
},
|
|
47
|
-
effort: '20-30 minutes'
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
method: 'execFile',
|
|
51
|
-
dangerous: true,
|
|
52
|
-
vulnerability: 'command-injection',
|
|
53
|
-
safeAlternatives: ['spawn'],
|
|
54
|
-
example: {
|
|
55
|
-
bad: 'execFile(userCommand, userArgs, callback)',
|
|
56
|
-
good: [
|
|
57
|
-
'spawn(validatedCommand, validatedArgs, {shell: false})',
|
|
58
|
-
'// Validate command and args first'
|
|
59
|
-
]
|
|
60
|
-
},
|
|
61
|
-
effort: '10-15 minutes'
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
method: 'execFileSync',
|
|
65
|
-
dangerous: true,
|
|
66
|
-
vulnerability: 'command-injection',
|
|
67
|
-
safeAlternatives: ['spawnSync'],
|
|
68
|
-
example: {
|
|
69
|
-
bad: 'execFileSync(userCommand, userArgs)',
|
|
70
|
-
good: [
|
|
71
|
-
'spawnSync(validatedCommand, validatedArgs, {shell: false})',
|
|
72
|
-
'// Validate command and args first'
|
|
73
|
-
]
|
|
74
|
-
},
|
|
75
|
-
effort: '10-15 minutes'
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
method: 'spawnSync',
|
|
79
|
-
dangerous: false,
|
|
80
|
-
vulnerability: 'argument-injection',
|
|
81
|
-
safeAlternatives: ['spawnSync with validation'],
|
|
82
|
-
example: {
|
|
83
|
-
bad: 'spawnSync(\'bash\', [\'-c\', userCommand])',
|
|
84
|
-
good: [
|
|
85
|
-
'spawnSync(validatedCommand, validatedArgs, {shell: false})',
|
|
86
|
-
'// Validate command and args first'
|
|
87
|
-
]
|
|
88
|
-
},
|
|
89
|
-
effort: '15-20 minutes'
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
method: 'fork',
|
|
93
|
-
dangerous: true,
|
|
94
|
-
vulnerability: 'command-injection',
|
|
95
|
-
safeAlternatives: ['spawn'],
|
|
96
|
-
example: {
|
|
97
|
-
bad: 'fork(userScript)',
|
|
98
|
-
good: [
|
|
99
|
-
'spawn(\'node\', [validatedScript], {shell: false})',
|
|
100
|
-
'// Validate script path first'
|
|
101
|
-
]
|
|
102
|
-
},
|
|
103
|
-
effort: '15-20 minutes'
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
method: 'forkSync',
|
|
107
|
-
dangerous: true,
|
|
108
|
-
vulnerability: 'command-injection',
|
|
109
|
-
safeAlternatives: ['spawnSync'],
|
|
110
|
-
example: {
|
|
111
|
-
bad: 'forkSync(userScript)',
|
|
112
|
-
good: [
|
|
113
|
-
'spawnSync(\'node\', [validatedScript], {shell: false, stdio: \'inherit\'})',
|
|
114
|
-
'// Validate script path first'
|
|
115
|
-
]
|
|
116
|
-
},
|
|
117
|
-
effort: '15-20 minutes'
|
|
118
|
-
}
|
|
119
|
-
];
|
|
120
|
-
const generateRefactoringSteps = (pattern) => {
|
|
121
|
-
switch (pattern.method) {
|
|
122
|
-
case 'exec':
|
|
123
|
-
case 'execSync':
|
|
124
|
-
return [
|
|
125
|
-
' 1. Replace exec() with execFile() or spawn()',
|
|
126
|
-
' 2. Split command and arguments into separate array elements',
|
|
127
|
-
' 3. Use {shell: false} option to prevent shell interpretation',
|
|
128
|
-
' 4. Validate and sanitize all user inputs',
|
|
129
|
-
' 5. Consider using execa library for better security'
|
|
130
|
-
].join('\n');
|
|
131
|
-
case 'spawn':
|
|
132
|
-
return [
|
|
133
|
-
' 1. Ensure first argument is a safe, validated command path',
|
|
134
|
-
' 2. Pass arguments as separate array elements',
|
|
135
|
-
' 3. Use {shell: false} to prevent shell injection',
|
|
136
|
-
' 4. Validate command exists and is executable',
|
|
137
|
-
' 5. Consider using cross-spawn for cross-platform safety'
|
|
138
|
-
].join('\n');
|
|
139
|
-
case 'execFile':
|
|
140
|
-
return [
|
|
141
|
-
' 1. Replace execFile() with spawn() for better security',
|
|
142
|
-
' 2. Validate command path before execution',
|
|
143
|
-
' 3. Ensure arguments are properly sanitized',
|
|
144
|
-
' 4. Use {shell: false} option',
|
|
145
|
-
' 5. Consider using execa library'
|
|
146
|
-
].join('\n');
|
|
147
|
-
case 'execFileSync':
|
|
148
|
-
return [
|
|
149
|
-
' 1. Replace execFileSync() with spawnSync() for better security',
|
|
150
|
-
' 2. Validate command path before execution',
|
|
151
|
-
' 3. Ensure arguments are properly sanitized',
|
|
152
|
-
' 4. Use {shell: false} option',
|
|
153
|
-
' 5. Consider using execa library'
|
|
154
|
-
].join('\n');
|
|
155
|
-
case 'spawnSync':
|
|
156
|
-
return [
|
|
157
|
-
' 1. Ensure first argument is a safe, validated command path',
|
|
158
|
-
' 2. Pass arguments as separate array elements',
|
|
159
|
-
' 3. Use {shell: false} to prevent shell injection',
|
|
160
|
-
' 4. Validate command exists and is executable',
|
|
161
|
-
' 5. Handle synchronous execution properly'
|
|
162
|
-
].join('\n');
|
|
163
|
-
case 'fork':
|
|
164
|
-
return [
|
|
165
|
-
' 1. Replace fork() with spawn() for Node.js scripts',
|
|
166
|
-
' 2. Validate script path exists and is readable',
|
|
167
|
-
' 3. Use spawn(\'node\', [scriptPath], options) instead',
|
|
168
|
-
' 4. Add proper error handling',
|
|
169
|
-
' 5. Consider using child_process.execFile() for simple scripts'
|
|
170
|
-
].join('\n');
|
|
171
|
-
case 'forkSync':
|
|
172
|
-
return [
|
|
173
|
-
' 1. Replace forkSync() with spawnSync() for Node.js scripts',
|
|
174
|
-
' 2. Validate script path exists and is readable',
|
|
175
|
-
' 3. Use spawnSync(\'node\', [scriptPath], options) instead',
|
|
176
|
-
' 4. Add proper error handling and synchronous waiting',
|
|
177
|
-
' 5. Consider using child_process.execFileSync() for simple scripts'
|
|
178
|
-
].join('\n');
|
|
179
|
-
default:
|
|
180
|
-
return [
|
|
181
|
-
' 1. Identify the specific command execution need',
|
|
182
|
-
' 2. Choose appropriate child_process method',
|
|
183
|
-
' 3. Use argument arrays instead of string interpolation',
|
|
184
|
-
' 4. Add comprehensive input validation',
|
|
185
|
-
' 5. Test with malicious inputs'
|
|
186
|
-
].join('\n');
|
|
187
|
-
}
|
|
188
|
-
};
|
|
189
|
-
exports.generateRefactoringSteps = generateRefactoringSteps;
|
|
190
|
-
exports.detectChildProcess = (0, eslint_devkit_2.createRule)({
|
|
191
|
-
name: 'detect-child-process',
|
|
192
|
-
meta: {
|
|
193
|
-
type: 'problem',
|
|
194
|
-
docs: {
|
|
195
|
-
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/detect-child-process.md',
|
|
196
|
-
description: 'Detects child_process usage that may allow command injection',
|
|
197
|
-
cwe: 'CWE-78',
|
|
198
|
-
cvss: 9.8,
|
|
199
|
-
confidence: 'medium',
|
|
200
|
-
},
|
|
201
|
-
messages: {
|
|
202
|
-
childProcessCommandInjection: (0, eslint_devkit_1.formatLLMMessage)({
|
|
203
|
-
icon: eslint_devkit_1.MessageIcons.WARNING,
|
|
204
|
-
issueName: 'Command injection',
|
|
205
|
-
cwe: 'CWE-78',
|
|
206
|
-
description: 'Command injection detected',
|
|
207
|
-
severity: 'CRITICAL',
|
|
208
|
-
fix: 'Use execFile/spawn with {shell: false} and array args',
|
|
209
|
-
documentationLink: 'https://owasp.org/www-community/attacks/Command_Injection',
|
|
210
|
-
}),
|
|
211
|
-
useExecFile: (0, eslint_devkit_1.formatLLMMessage)({
|
|
212
|
-
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
213
|
-
issueName: 'Use execFile',
|
|
214
|
-
description: 'Use execFile() with argument array',
|
|
215
|
-
severity: 'LOW',
|
|
216
|
-
fix: 'execFile(cmd, [arg1, arg2], { shell: false })',
|
|
217
|
-
documentationLink: 'https://nodejs.org/api/child_process.html#child_processexecfilefile-args-options-callback',
|
|
218
|
-
}),
|
|
219
|
-
useSpawn: (0, eslint_devkit_1.formatLLMMessage)({
|
|
220
|
-
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
221
|
-
issueName: 'Use spawn',
|
|
222
|
-
description: 'Use spawn() with separate arguments',
|
|
223
|
-
severity: 'LOW',
|
|
224
|
-
fix: 'spawn(cmd, [arg1, arg2], { shell: false })',
|
|
225
|
-
documentationLink: 'https://nodejs.org/api/child_process.html#child_processspawncommand-args-options',
|
|
226
|
-
}),
|
|
227
|
-
useSaferLibrary: (0, eslint_devkit_1.formatLLMMessage)({
|
|
228
|
-
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
229
|
-
issueName: 'Use Safer Library',
|
|
230
|
-
description: 'Consider safer command execution libraries',
|
|
231
|
-
severity: 'LOW',
|
|
232
|
-
fix: 'Use execa, zx, or cross-spawn instead',
|
|
233
|
-
documentationLink: 'https://github.com/sindresorhus/execa',
|
|
234
|
-
}),
|
|
235
|
-
validateInput: (0, eslint_devkit_1.formatLLMMessage)({
|
|
236
|
-
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
237
|
-
issueName: 'Validate Input',
|
|
238
|
-
description: 'Add input validation and sanitization',
|
|
239
|
-
severity: 'LOW',
|
|
240
|
-
fix: 'Validate user input before passing to command',
|
|
241
|
-
documentationLink: 'https://owasp.org/www-community/attacks/Command_Injection',
|
|
242
|
-
}),
|
|
243
|
-
useShellFalse: (0, eslint_devkit_1.formatLLMMessage)({
|
|
244
|
-
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
245
|
-
issueName: 'Disable Shell',
|
|
246
|
-
description: 'Use shell: false option',
|
|
247
|
-
severity: 'LOW',
|
|
248
|
-
fix: '{ shell: false } to prevent shell interpretation',
|
|
249
|
-
documentationLink: 'https://nodejs.org/api/child_process.html#spawning-bat-and-cmd-files-on-windows',
|
|
250
|
-
}),
|
|
251
|
-
strategyValidate: (0, eslint_devkit_1.formatLLMMessage)({
|
|
252
|
-
icon: eslint_devkit_1.MessageIcons.STRATEGY,
|
|
253
|
-
issueName: 'Validate Strategy',
|
|
254
|
-
description: 'Comprehensive input validation',
|
|
255
|
-
severity: 'LOW',
|
|
256
|
-
fix: 'Add allowlist validation before command execution',
|
|
257
|
-
documentationLink: 'https://owasp.org/www-community/attacks/Command_Injection',
|
|
258
|
-
}),
|
|
259
|
-
strategySanitize: (0, eslint_devkit_1.formatLLMMessage)({
|
|
260
|
-
icon: eslint_devkit_1.MessageIcons.STRATEGY,
|
|
261
|
-
issueName: 'Sanitize Strategy',
|
|
262
|
-
description: 'Sanitize and escape command arguments',
|
|
263
|
-
severity: 'LOW',
|
|
264
|
-
fix: 'Escape special characters in command arguments',
|
|
265
|
-
documentationLink: 'https://owasp.org/www-community/attacks/Command_Injection',
|
|
266
|
-
}),
|
|
267
|
-
strategyRestrict: (0, eslint_devkit_1.formatLLMMessage)({
|
|
268
|
-
icon: eslint_devkit_1.MessageIcons.STRATEGY,
|
|
269
|
-
issueName: 'Restrict Strategy',
|
|
270
|
-
description: 'Restrict to predefined safe commands',
|
|
271
|
-
severity: 'LOW',
|
|
272
|
-
fix: 'Define allowlist of permitted commands',
|
|
273
|
-
documentationLink: 'https://owasp.org/www-community/attacks/Command_Injection',
|
|
274
|
-
})
|
|
275
|
-
},
|
|
276
|
-
schema: [
|
|
277
|
-
{
|
|
278
|
-
type: 'object',
|
|
279
|
-
properties: {
|
|
280
|
-
allowLiteralStrings: {
|
|
281
|
-
type: 'boolean',
|
|
282
|
-
default: false,
|
|
283
|
-
description: 'Allow exec() with literal strings'
|
|
284
|
-
},
|
|
285
|
-
allowLiteralSpawn: {
|
|
286
|
-
type: 'boolean',
|
|
287
|
-
default: false,
|
|
288
|
-
description: 'Allow spawn() with literal arguments'
|
|
289
|
-
},
|
|
290
|
-
additionalMethods: {
|
|
291
|
-
type: 'array',
|
|
292
|
-
items: { type: 'string' },
|
|
293
|
-
default: [],
|
|
294
|
-
description: 'Additional child_process methods to check'
|
|
295
|
-
},
|
|
296
|
-
strategy: {
|
|
297
|
-
type: 'string',
|
|
298
|
-
enum: ['validate', 'sanitize', 'restrict', 'auto'],
|
|
299
|
-
default: 'auto',
|
|
300
|
-
description: 'Strategy for fixing command injection (auto = smart detection)'
|
|
301
|
-
}
|
|
302
|
-
},
|
|
303
|
-
additionalProperties: false,
|
|
304
|
-
},
|
|
305
|
-
],
|
|
306
|
-
},
|
|
307
|
-
defaultOptions: [
|
|
308
|
-
{
|
|
309
|
-
allowLiteralStrings: false,
|
|
310
|
-
allowLiteralSpawn: false,
|
|
311
|
-
additionalMethods: [],
|
|
312
|
-
strategy: 'auto'
|
|
313
|
-
},
|
|
314
|
-
],
|
|
315
|
-
create(context) {
|
|
316
|
-
const options = context.options[0] || {};
|
|
317
|
-
const { allowLiteralStrings = false, allowLiteralSpawn = false, additionalMethods = [], } = options;
|
|
318
|
-
const dangerousMethodsSet = new Set([
|
|
319
|
-
'exec',
|
|
320
|
-
'execSync',
|
|
321
|
-
'execFile',
|
|
322
|
-
'execFileSync',
|
|
323
|
-
'spawn',
|
|
324
|
-
'spawnSync',
|
|
325
|
-
'fork',
|
|
326
|
-
'forkSync',
|
|
327
|
-
...additionalMethods
|
|
328
|
-
]);
|
|
329
|
-
const moduleAliases = new Set(['child_process']);
|
|
330
|
-
const importedMethods = new Set();
|
|
331
|
-
const containsDynamicStrings = (node) => {
|
|
332
|
-
if (node.type === 'TemplateLiteral') {
|
|
333
|
-
return node.expressions.length > 0;
|
|
334
|
-
}
|
|
335
|
-
if (node.type === 'BinaryExpression' && node.operator === '+') {
|
|
336
|
-
return true;
|
|
337
|
-
}
|
|
338
|
-
if (node.type === 'Identifier') {
|
|
339
|
-
return true;
|
|
340
|
-
}
|
|
341
|
-
return false;
|
|
342
|
-
};
|
|
343
|
-
const hasOnlyLiteralArgs = (args) => {
|
|
344
|
-
if (args.length === 0)
|
|
345
|
-
return false;
|
|
346
|
-
const command = args[0];
|
|
347
|
-
if (command.type !== 'Literal' || typeof command.value !== 'string') {
|
|
348
|
-
return false;
|
|
349
|
-
}
|
|
350
|
-
if (args.length >= 2) {
|
|
351
|
-
const argsArray = args[1];
|
|
352
|
-
if (argsArray.type === 'ArrayExpression') {
|
|
353
|
-
const allLiteralElements = argsArray.elements.every((el) => el?.type === 'Literal' && typeof el.value === 'string');
|
|
354
|
-
if (!allLiteralElements) {
|
|
355
|
-
return false;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
else if (argsArray.type !== 'Literal') {
|
|
359
|
-
return false;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
return true;
|
|
363
|
-
};
|
|
364
|
-
const hasShellFalseOption = (node) => {
|
|
365
|
-
const optionsArg = node.arguments[2];
|
|
366
|
-
if (!optionsArg || optionsArg.type !== eslint_devkit_1.AST_NODE_TYPES.ObjectExpression) {
|
|
367
|
-
return true;
|
|
368
|
-
}
|
|
369
|
-
for (const prop of optionsArg.properties) {
|
|
370
|
-
if (prop.type === eslint_devkit_1.AST_NODE_TYPES.Property &&
|
|
371
|
-
prop.key.type === eslint_devkit_1.AST_NODE_TYPES.Identifier &&
|
|
372
|
-
prop.key.name === 'shell') {
|
|
373
|
-
if (prop.value.type === eslint_devkit_1.AST_NODE_TYPES.Literal && prop.value.value === false) {
|
|
374
|
-
return true;
|
|
375
|
-
}
|
|
376
|
-
return false;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
return true;
|
|
380
|
-
};
|
|
381
|
-
const hasPrecedingAllowlistValidation = (node) => {
|
|
382
|
-
const makeArgChecker = (validatedVarNames) => {
|
|
383
|
-
const check = (argNode) => {
|
|
384
|
-
if (argNode.type === 'Identifier' && validatedVarNames.has(argNode.name))
|
|
385
|
-
return true;
|
|
386
|
-
if (argNode.type === 'TemplateLiteral') {
|
|
387
|
-
return argNode.expressions.some(e => e.type === 'Identifier' && validatedVarNames.has(e.name));
|
|
388
|
-
}
|
|
389
|
-
if (argNode.type === eslint_devkit_1.AST_NODE_TYPES.ArrayExpression) {
|
|
390
|
-
return argNode.elements.some(el => el !== null && check(el));
|
|
391
|
-
}
|
|
392
|
-
return false;
|
|
393
|
-
};
|
|
394
|
-
return check;
|
|
395
|
-
};
|
|
396
|
-
const checkGuardClause = (ifNode) => {
|
|
397
|
-
const test = ifNode.test;
|
|
398
|
-
if (test.type === 'CallExpression' &&
|
|
399
|
-
test.callee.type === 'MemberExpression' &&
|
|
400
|
-
test.callee.property.type === 'Identifier' &&
|
|
401
|
-
test.callee.property.name === 'includes') {
|
|
402
|
-
const validatedVarNames = new Set();
|
|
403
|
-
for (const testArg of test.arguments) {
|
|
404
|
-
if (testArg.type === 'Identifier')
|
|
405
|
-
validatedVarNames.add(testArg.name);
|
|
406
|
-
}
|
|
407
|
-
const check = makeArgChecker(validatedVarNames);
|
|
408
|
-
for (const arg of node.arguments) {
|
|
409
|
-
if (check(arg))
|
|
410
|
-
return true;
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
if (test.type === eslint_devkit_1.AST_NODE_TYPES.UnaryExpression && test.operator === '!' &&
|
|
414
|
-
test.argument.type === eslint_devkit_1.AST_NODE_TYPES.CallExpression &&
|
|
415
|
-
test.argument.callee.type === eslint_devkit_1.AST_NODE_TYPES.MemberExpression &&
|
|
416
|
-
test.argument.callee.property.type === eslint_devkit_1.AST_NODE_TYPES.Identifier &&
|
|
417
|
-
test.argument.callee.property.name === 'includes') {
|
|
418
|
-
const consequent = ifNode.consequent;
|
|
419
|
-
const isGuardBody = (consequent.type === eslint_devkit_1.AST_NODE_TYPES.ReturnStatement ||
|
|
420
|
-
consequent.type === eslint_devkit_1.AST_NODE_TYPES.ThrowStatement ||
|
|
421
|
-
(consequent.type === eslint_devkit_1.AST_NODE_TYPES.BlockStatement &&
|
|
422
|
-
consequent.body.length > 0 &&
|
|
423
|
-
(consequent.body[0].type === eslint_devkit_1.AST_NODE_TYPES.ReturnStatement ||
|
|
424
|
-
consequent.body[0].type === eslint_devkit_1.AST_NODE_TYPES.ThrowStatement)));
|
|
425
|
-
if (isGuardBody) {
|
|
426
|
-
const validatedVarNames = new Set();
|
|
427
|
-
for (const testArg of test.argument.arguments) {
|
|
428
|
-
if (testArg.type === 'Identifier')
|
|
429
|
-
validatedVarNames.add(testArg.name);
|
|
430
|
-
}
|
|
431
|
-
const check = makeArgChecker(validatedVarNames.size > 0 ? validatedVarNames : new Set(['*']));
|
|
432
|
-
if (validatedVarNames.size > 0) {
|
|
433
|
-
for (const arg of node.arguments) {
|
|
434
|
-
if (check(arg))
|
|
435
|
-
return true;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
else {
|
|
439
|
-
for (const arg of node.arguments) {
|
|
440
|
-
if (arg.type === 'Identifier' ||
|
|
441
|
-
(arg.type === eslint_devkit_1.AST_NODE_TYPES.ArrayExpression && arg.elements.some(el => el?.type === 'Identifier'))) {
|
|
442
|
-
return true;
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
return false;
|
|
449
|
-
};
|
|
450
|
-
let current = node.parent;
|
|
451
|
-
while (current) {
|
|
452
|
-
if (current.type === 'IfStatement') {
|
|
453
|
-
if (checkGuardClause(current))
|
|
454
|
-
return true;
|
|
455
|
-
}
|
|
456
|
-
current = current.parent;
|
|
457
|
-
}
|
|
458
|
-
let stmt = node.parent;
|
|
459
|
-
while (stmt && stmt.parent && stmt.parent.type !== eslint_devkit_1.AST_NODE_TYPES.BlockStatement) {
|
|
460
|
-
stmt = stmt.parent;
|
|
461
|
-
}
|
|
462
|
-
if (stmt && stmt.parent && stmt.parent.type === eslint_devkit_1.AST_NODE_TYPES.BlockStatement) {
|
|
463
|
-
const block = stmt.parent;
|
|
464
|
-
const callIndex = block.body.indexOf(stmt);
|
|
465
|
-
if (callIndex > 0) {
|
|
466
|
-
for (let i = 0; i < callIndex; i++) {
|
|
467
|
-
const sibling = block.body[i];
|
|
468
|
-
if (sibling.type === 'IfStatement') {
|
|
469
|
-
if (checkGuardClause(sibling))
|
|
470
|
-
return true;
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
return false;
|
|
476
|
-
};
|
|
477
|
-
const extractCommandInfo = (node, method) => {
|
|
478
|
-
const sourceCode = context.sourceCode;
|
|
479
|
-
const args = node.arguments.map((arg) => sourceCode.getText(arg)).join(', ');
|
|
480
|
-
const pattern = COMMAND_PATTERNS.find(p => p.method === method) || null;
|
|
481
|
-
const isDynamic = node.arguments.some((arg) => containsDynamicStrings(arg));
|
|
482
|
-
return { args, pattern, isDynamic };
|
|
483
|
-
};
|
|
484
|
-
const determineRiskLevel = (pattern, isDynamic) => {
|
|
485
|
-
if (pattern?.dangerous && isDynamic) {
|
|
486
|
-
return 'critical';
|
|
487
|
-
}
|
|
488
|
-
if (pattern?.dangerous || isDynamic) {
|
|
489
|
-
return 'high';
|
|
490
|
-
}
|
|
491
|
-
return 'medium';
|
|
492
|
-
};
|
|
493
|
-
const getChildProcessCall = (node) => {
|
|
494
|
-
if (node.callee.type === 'MemberExpression' &&
|
|
495
|
-
node.callee.property.type === 'Identifier') {
|
|
496
|
-
const methodName = node.callee.property.name;
|
|
497
|
-
if (!dangerousMethodsSet.has(methodName)) {
|
|
498
|
-
return null;
|
|
499
|
-
}
|
|
500
|
-
if (node.callee.object.type === 'Identifier' &&
|
|
501
|
-
moduleAliases.has(node.callee.object.name)) {
|
|
502
|
-
return { method: methodName, calleeNode: node.callee };
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
if (node.callee.type === 'Identifier' && dangerousMethodsSet.has(node.callee.name)) {
|
|
506
|
-
if (importedMethods.has(node.callee.name)) {
|
|
507
|
-
return { method: node.callee.name, calleeNode: node.callee };
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
return null;
|
|
511
|
-
};
|
|
512
|
-
const checkChildProcessCall = (node) => {
|
|
513
|
-
const detected = getChildProcessCall(node);
|
|
514
|
-
if (!detected) {
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
const { method } = detected;
|
|
518
|
-
const { args, pattern, isDynamic } = extractCommandInfo(node, method);
|
|
519
|
-
if ((method === 'exec' || method === 'execSync') && !isDynamic && hasOnlyLiteralArgs(node.arguments)) {
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
if (allowLiteralStrings && method === 'exec' && !isDynamic) {
|
|
523
|
-
return;
|
|
524
|
-
}
|
|
525
|
-
const saferMethods = new Set(['spawn', 'spawnSync', 'execFile', 'execFileSync']);
|
|
526
|
-
if (allowLiteralSpawn && saferMethods.has(method) && hasOnlyLiteralArgs(node.arguments)) {
|
|
527
|
-
return;
|
|
528
|
-
}
|
|
529
|
-
if (saferMethods.has(method) && hasOnlyLiteralArgs(node.arguments)) {
|
|
530
|
-
const isExecFile = method === 'execFile' || method === 'execFileSync';
|
|
531
|
-
if (isExecFile || hasShellFalseOption(node)) {
|
|
532
|
-
return;
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
const allSafeMethods = ['execFile', 'execFileSync', 'spawn', 'spawnSync'];
|
|
536
|
-
if (allSafeMethods.includes(method) && hasPrecedingAllowlistValidation(node)) {
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
const riskLevel = determineRiskLevel(pattern, isDynamic);
|
|
540
|
-
const steps = pattern ? (0, exports.generateRefactoringSteps)(pattern) : 'Review and secure command execution';
|
|
541
|
-
const alternatives = pattern?.safeAlternatives.join(', ') || 'execFile, spawn with validation';
|
|
542
|
-
context.report({
|
|
543
|
-
node,
|
|
544
|
-
messageId: 'childProcessCommandInjection',
|
|
545
|
-
data: {
|
|
546
|
-
method,
|
|
547
|
-
args,
|
|
548
|
-
riskLevel,
|
|
549
|
-
vulnerability: pattern?.vulnerability || 'command injection',
|
|
550
|
-
alternatives,
|
|
551
|
-
steps,
|
|
552
|
-
effort: pattern?.effort || '15-30 minutes'
|
|
553
|
-
},
|
|
554
|
-
suggest: [
|
|
555
|
-
{
|
|
556
|
-
messageId: 'useExecFile',
|
|
557
|
-
fix: () => null
|
|
558
|
-
},
|
|
559
|
-
{
|
|
560
|
-
messageId: 'useSpawn',
|
|
561
|
-
fix: () => null
|
|
562
|
-
},
|
|
563
|
-
{
|
|
564
|
-
messageId: 'useSaferLibrary',
|
|
565
|
-
fix: () => null
|
|
566
|
-
},
|
|
567
|
-
{
|
|
568
|
-
messageId: 'validateInput',
|
|
569
|
-
fix: () => null
|
|
570
|
-
},
|
|
571
|
-
{
|
|
572
|
-
messageId: 'useShellFalse',
|
|
573
|
-
fix: () => null
|
|
574
|
-
}
|
|
575
|
-
]
|
|
576
|
-
});
|
|
577
|
-
};
|
|
578
|
-
const trackChildProcessImport = (node) => {
|
|
579
|
-
if (node.source.value !== 'child_process') {
|
|
580
|
-
return;
|
|
581
|
-
}
|
|
582
|
-
for (const specifier of node.specifiers) {
|
|
583
|
-
if (specifier.type === 'ImportDefaultSpecifier' || specifier.type === 'ImportNamespaceSpecifier') {
|
|
584
|
-
moduleAliases.add(specifier.local.name);
|
|
585
|
-
}
|
|
586
|
-
if (specifier.type === 'ImportSpecifier') {
|
|
587
|
-
importedMethods.add(specifier.local.name);
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
};
|
|
591
|
-
const trackChildProcessRequire = (node) => {
|
|
592
|
-
if (!node.init) {
|
|
593
|
-
return;
|
|
594
|
-
}
|
|
595
|
-
if (node.id.type === 'Identifier' &&
|
|
596
|
-
node.init.type === 'CallExpression' &&
|
|
597
|
-
node.init.callee.type === 'Identifier' &&
|
|
598
|
-
node.init.callee.name === 'require' &&
|
|
599
|
-
node.init.arguments[0] &&
|
|
600
|
-
node.init.arguments[0].type === 'Literal' &&
|
|
601
|
-
node.init.arguments[0].value === 'child_process') {
|
|
602
|
-
moduleAliases.add(node.id.name);
|
|
603
|
-
return;
|
|
604
|
-
}
|
|
605
|
-
if (node.id.type === 'ObjectPattern' &&
|
|
606
|
-
node.init?.type === 'CallExpression' &&
|
|
607
|
-
node.init.callee.type === 'Identifier' &&
|
|
608
|
-
node.init.callee.name === 'require' &&
|
|
609
|
-
node.init.arguments[0] &&
|
|
610
|
-
node.init.arguments[0].type === 'Literal' &&
|
|
611
|
-
node.init.arguments[0].value === 'child_process') {
|
|
612
|
-
for (const prop of node.id.properties) {
|
|
613
|
-
if (prop.type === 'Property' && prop.key.type === 'Identifier') {
|
|
614
|
-
importedMethods.add(prop.value.type === 'Identifier' ? prop.value.name : prop.key.name);
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
};
|
|
619
|
-
return {
|
|
620
|
-
CallExpression: checkChildProcessCall,
|
|
621
|
-
ImportDeclaration: trackChildProcessImport,
|
|
622
|
-
VariableDeclarator: trackChildProcessRequire
|
|
623
|
-
};
|
|
624
|
-
},
|
|
625
|
-
});
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.detectChildProcess=exports.generateRefactoringSteps=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const eslint_devkit_2=require("@interlace/eslint-devkit");const COMMAND_PATTERNS=[{method:"exec",dangerous:true,vulnerability:"command-injection",safeAlternatives:["execFile","spawn"],example:{bad:"exec(`git clone ${repoUrl}`)",good:["execFile('git', ['clone', repoUrl], {shell: false})","spawn('git', ['clone', repoUrl], {shell: false})"]},effort:"15-25 minutes"},{method:"execSync",dangerous:true,vulnerability:"command-injection",safeAlternatives:["execFileSync","spawnSync"],example:{bad:"execSync(`npm install ${packageName}`)",good:["execFileSync('npm', ['install', packageName], {shell: false})","spawnSync('npm', ['install', packageName], {shell: false})"]},effort:"15-25 minutes"},{method:"spawn",dangerous:false,vulnerability:"argument-injection",safeAlternatives:["spawn with validation"],example:{bad:"spawn('bash', ['-c', userCommand])",good:["spawn(validatedCommand, validatedArgs, {shell: false})","// Validate command and args first"]},effort:"20-30 minutes"},{method:"execFile",dangerous:true,vulnerability:"command-injection",safeAlternatives:["spawn"],example:{bad:"execFile(userCommand, userArgs, callback)",good:["spawn(validatedCommand, validatedArgs, {shell: false})","// Validate command and args first"]},effort:"10-15 minutes"},{method:"execFileSync",dangerous:true,vulnerability:"command-injection",safeAlternatives:["spawnSync"],example:{bad:"execFileSync(userCommand, userArgs)",good:["spawnSync(validatedCommand, validatedArgs, {shell: false})","// Validate command and args first"]},effort:"10-15 minutes"},{method:"spawnSync",dangerous:false,vulnerability:"argument-injection",safeAlternatives:["spawnSync with validation"],example:{bad:"spawnSync('bash', ['-c', userCommand])",good:["spawnSync(validatedCommand, validatedArgs, {shell: false})","// Validate command and args first"]},effort:"15-20 minutes"},{method:"fork",dangerous:true,vulnerability:"command-injection",safeAlternatives:["spawn"],example:{bad:"fork(userScript)",good:["spawn('node', [validatedScript], {shell: false})","// Validate script path first"]},effort:"15-20 minutes"},{method:"forkSync",dangerous:true,vulnerability:"command-injection",safeAlternatives:["spawnSync"],example:{bad:"forkSync(userScript)",good:["spawnSync('node', [validatedScript], {shell: false, stdio: 'inherit'})","// Validate script path first"]},effort:"15-20 minutes"}];const generateRefactoringSteps=pattern=>{switch(pattern.method){case"exec":case"execSync":return[" 1. Replace exec() with execFile() or spawn()"," 2. Split command and arguments into separate array elements"," 3. Use {shell: false} option to prevent shell interpretation"," 4. Validate and sanitize all user inputs"," 5. Consider using execa library for better security"].join("\n");case"spawn":return[" 1. Ensure first argument is a safe, validated command path"," 2. Pass arguments as separate array elements"," 3. Use {shell: false} to prevent shell injection"," 4. Validate command exists and is executable"," 5. Consider using cross-spawn for cross-platform safety"].join("\n");case"execFile":return[" 1. Replace execFile() with spawn() for better security"," 2. Validate command path before execution"," 3. Ensure arguments are properly sanitized"," 4. Use {shell: false} option"," 5. Consider using execa library"].join("\n");case"execFileSync":return[" 1. Replace execFileSync() with spawnSync() for better security"," 2. Validate command path before execution"," 3. Ensure arguments are properly sanitized"," 4. Use {shell: false} option"," 5. Consider using execa library"].join("\n");case"spawnSync":return[" 1. Ensure first argument is a safe, validated command path"," 2. Pass arguments as separate array elements"," 3. Use {shell: false} to prevent shell injection"," 4. Validate command exists and is executable"," 5. Handle synchronous execution properly"].join("\n");case"fork":return[" 1. Replace fork() with spawn() for Node.js scripts"," 2. Validate script path exists and is readable"," 3. Use spawn('node', [scriptPath], options) instead"," 4. Add proper error handling"," 5. Consider using child_process.execFile() for simple scripts"].join("\n");case"forkSync":return[" 1. Replace forkSync() with spawnSync() for Node.js scripts"," 2. Validate script path exists and is readable"," 3. Use spawnSync('node', [scriptPath], options) instead"," 4. Add proper error handling and synchronous waiting"," 5. Consider using child_process.execFileSync() for simple scripts"].join("\n");default:return[" 1. Identify the specific command execution need"," 2. Choose appropriate child_process method"," 3. Use argument arrays instead of string interpolation"," 4. Add comprehensive input validation"," 5. Test with malicious inputs"].join("\n")}};exports.generateRefactoringSteps=generateRefactoringSteps;exports.detectChildProcess=(0,eslint_devkit_2.createRule)({name:"detect-child-process",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-node-security/docs/rules/detect-child-process.md",description:"Detects child_process usage that may allow command injection",cwe:"CWE-78",cvss:9.8,confidence:"medium"},messages:{childProcessCommandInjection:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.WARNING,issueName:"Command injection",cwe:"CWE-78",description:"Command injection detected",severity:"CRITICAL",fix:"Use execFile/spawn with {shell: false} and array args",documentationLink:"https://owasp.org/www-community/attacks/Command_Injection"}),useExecFile:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use execFile",description:"Use execFile() with argument array",severity:"LOW",fix:"execFile(cmd, [arg1, arg2], { shell: false })",documentationLink:"https://nodejs.org/api/child_process.html#child_processexecfilefile-args-options-callback"}),useSpawn:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use spawn",description:"Use spawn() with separate arguments",severity:"LOW",fix:"spawn(cmd, [arg1, arg2], { shell: false })",documentationLink:"https://nodejs.org/api/child_process.html#child_processspawncommand-args-options"}),useSaferLibrary:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use Safer Library",description:"Consider safer command execution libraries",severity:"LOW",fix:"Use execa, zx, or cross-spawn instead",documentationLink:"https://github.com/sindresorhus/execa"}),validateInput:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Validate Input",description:"Add input validation and sanitization",severity:"LOW",fix:"Validate user input before passing to command",documentationLink:"https://owasp.org/www-community/attacks/Command_Injection"}),useShellFalse:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Disable Shell",description:"Use shell: false option",severity:"LOW",fix:"{ shell: false } to prevent shell interpretation",documentationLink:"https://nodejs.org/api/child_process.html#spawning-bat-and-cmd-files-on-windows"}),strategyValidate:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.STRATEGY,issueName:"Validate Strategy",description:"Comprehensive input validation",severity:"LOW",fix:"Add allowlist validation before command execution",documentationLink:"https://owasp.org/www-community/attacks/Command_Injection"}),strategySanitize:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.STRATEGY,issueName:"Sanitize Strategy",description:"Sanitize and escape command arguments",severity:"LOW",fix:"Escape special characters in command arguments",documentationLink:"https://owasp.org/www-community/attacks/Command_Injection"}),strategyRestrict:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.STRATEGY,issueName:"Restrict Strategy",description:"Restrict to predefined safe commands",severity:"LOW",fix:"Define allowlist of permitted commands",documentationLink:"https://owasp.org/www-community/attacks/Command_Injection"})},schema:[{type:"object",properties:{allowLiteralStrings:{type:"boolean",default:false,description:"Allow exec() with literal strings"},allowLiteralSpawn:{type:"boolean",default:false,description:"Allow spawn() with literal arguments"},additionalMethods:{type:"array",items:{type:"string"},default:[],description:"Additional child_process methods to check"},strategy:{type:"string",enum:["validate","sanitize","restrict","auto"],default:"auto",description:"Strategy for fixing command injection (auto = smart detection)"}},additionalProperties:false}]},defaultOptions:[{allowLiteralStrings:false,allowLiteralSpawn:false,additionalMethods:[],strategy:"auto"}],create(context){const options=context.options[0]||{};const{allowLiteralStrings=false,allowLiteralSpawn=false,additionalMethods=[]}=options;const dangerousMethodsSet=new Set(["exec","execSync","execFile","execFileSync","spawn","spawnSync","fork","forkSync",...additionalMethods]);const moduleAliases=new Set(["child_process"]);const importedMethods=new Set;const containsDynamicStrings=node=>{if(node.type==="TemplateLiteral"){return node.expressions.length>0}if(node.type==="BinaryExpression"&&node.operator==="+"){return true}if(node.type==="Identifier"){return true}return false};const hasOnlyLiteralArgs=args=>{if(args.length===0)return false;const command=args[0];if(command.type!=="Literal"||typeof command.value!=="string"){return false}if(args.length>=2){const argsArray=args[1];if(argsArray.type==="ArrayExpression"){const allLiteralElements=argsArray.elements.every(el=>el?.type==="Literal"&&typeof el.value==="string");if(!allLiteralElements){return false}}else if(argsArray.type!=="Literal"){return false}}return true};const hasShellFalseOption=node=>{const optionsArg=node.arguments[2];if(!optionsArg||optionsArg.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectExpression){return true}for(const prop of optionsArg.properties){if(prop.type===eslint_devkit_1.AST_NODE_TYPES.Property&&prop.key.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&prop.key.name==="shell"){if(prop.value.type===eslint_devkit_1.AST_NODE_TYPES.Literal&&prop.value.value===false){return true}return false}}return true};const hasPrecedingAllowlistValidation=node=>{const makeArgChecker=validatedVarNames=>{const check=argNode=>{if(argNode.type==="Identifier"&&validatedVarNames.has(argNode.name))return true;if(argNode.type==="TemplateLiteral"){return argNode.expressions.some(e=>e.type==="Identifier"&&validatedVarNames.has(e.name))}if(argNode.type===eslint_devkit_1.AST_NODE_TYPES.ArrayExpression){return argNode.elements.some(el=>el!==null&&check(el))}return false};return check};const checkGuardClause=ifNode=>{const test=ifNode.test;if(test.type==="CallExpression"&&test.callee.type==="MemberExpression"&&test.callee.property.type==="Identifier"&&test.callee.property.name==="includes"){const validatedVarNames=new Set;for(const testArg of test.arguments){if(testArg.type==="Identifier")validatedVarNames.add(testArg.name)}const check=makeArgChecker(validatedVarNames);for(const arg of node.arguments){if(check(arg))return true}}if(test.type===eslint_devkit_1.AST_NODE_TYPES.UnaryExpression&&test.operator==="!"&&test.argument.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&test.argument.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&test.argument.callee.property.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&test.argument.callee.property.name==="includes"){const consequent=ifNode.consequent;const isGuardBody=consequent.type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement||consequent.type===eslint_devkit_1.AST_NODE_TYPES.ThrowStatement||consequent.type===eslint_devkit_1.AST_NODE_TYPES.BlockStatement&&consequent.body.length>0&&(consequent.body[0].type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement||consequent.body[0].type===eslint_devkit_1.AST_NODE_TYPES.ThrowStatement);if(isGuardBody){const validatedVarNames=new Set;for(const testArg of test.argument.arguments){if(testArg.type==="Identifier")validatedVarNames.add(testArg.name)}const check=makeArgChecker(validatedVarNames.size>0?validatedVarNames:new Set(["*"]));if(validatedVarNames.size>0){for(const arg of node.arguments){if(check(arg))return true}}else{for(const arg of node.arguments){if(arg.type==="Identifier"||arg.type===eslint_devkit_1.AST_NODE_TYPES.ArrayExpression&&arg.elements.some(el=>el?.type==="Identifier")){return true}}}}}return false};let current=node.parent;while(current){if(current.type==="IfStatement"){if(checkGuardClause(current))return true}current=current.parent}let stmt=node.parent;while(stmt&&stmt.parent&&stmt.parent.type!==eslint_devkit_1.AST_NODE_TYPES.BlockStatement){stmt=stmt.parent}if(stmt&&stmt.parent&&stmt.parent.type===eslint_devkit_1.AST_NODE_TYPES.BlockStatement){const block=stmt.parent;const callIndex=block.body.indexOf(stmt);if(callIndex>0){for(let i=0;i<callIndex;i++){const sibling=block.body[i];if(sibling.type==="IfStatement"){if(checkGuardClause(sibling))return true}}}}return false};const extractCommandInfo=(node,method)=>{const sourceCode=context.sourceCode;const args=node.arguments.map(arg=>sourceCode.getText(arg)).join(", ");const pattern=COMMAND_PATTERNS.find(p=>p.method===method)||null;const isDynamic=node.arguments.some(arg=>containsDynamicStrings(arg));return{args,pattern,isDynamic}};const determineRiskLevel=(pattern,isDynamic)=>{if(pattern?.dangerous&&isDynamic){return"critical"}if(pattern?.dangerous||isDynamic){return"high"}return"medium"};const getChildProcessCall=node=>{if(node.callee.type==="MemberExpression"&&node.callee.property.type==="Identifier"){const methodName=node.callee.property.name;if(!dangerousMethodsSet.has(methodName)){return null}if(node.callee.object.type==="Identifier"&&moduleAliases.has(node.callee.object.name)){return{method:methodName,calleeNode:node.callee}}}if(node.callee.type==="Identifier"&&dangerousMethodsSet.has(node.callee.name)){if(importedMethods.has(node.callee.name)){return{method:node.callee.name,calleeNode:node.callee}}}return null};const checkChildProcessCall=node=>{const detected=getChildProcessCall(node);if(!detected){return}const{method}=detected;const{args,pattern,isDynamic}=extractCommandInfo(node,method);if((method==="exec"||method==="execSync")&&!isDynamic&&hasOnlyLiteralArgs(node.arguments)){return}if(allowLiteralStrings&&method==="exec"&&!isDynamic){return}const saferMethods=new Set(["spawn","spawnSync","execFile","execFileSync"]);if(allowLiteralSpawn&&saferMethods.has(method)&&hasOnlyLiteralArgs(node.arguments)){return}if(saferMethods.has(method)&&hasOnlyLiteralArgs(node.arguments)){const isExecFile=method==="execFile"||method==="execFileSync";if(isExecFile||hasShellFalseOption(node)){return}}const allSafeMethods=["execFile","execFileSync","spawn","spawnSync"];if(allSafeMethods.includes(method)&&hasPrecedingAllowlistValidation(node)){return}const riskLevel=determineRiskLevel(pattern,isDynamic);const steps=pattern?(0,exports.generateRefactoringSteps)(pattern):"Review and secure command execution";const alternatives=pattern?.safeAlternatives.join(", ")||"execFile, spawn with validation";context.report({node,messageId:"childProcessCommandInjection",data:{method,args,riskLevel,vulnerability:pattern?.vulnerability||"command injection",alternatives,steps,effort:pattern?.effort||"15-30 minutes"},suggest:[{messageId:"useExecFile",fix:()=>null},{messageId:"useSpawn",fix:()=>null},{messageId:"useSaferLibrary",fix:()=>null},{messageId:"validateInput",fix:()=>null},{messageId:"useShellFalse",fix:()=>null}]})};const trackChildProcessImport=node=>{if(node.source.value!=="child_process"){return}for(const specifier of node.specifiers){if(specifier.type==="ImportDefaultSpecifier"||specifier.type==="ImportNamespaceSpecifier"){moduleAliases.add(specifier.local.name)}if(specifier.type==="ImportSpecifier"){importedMethods.add(specifier.local.name)}}};const trackChildProcessRequire=node=>{if(!node.init){return}if(node.id.type==="Identifier"&&node.init.type==="CallExpression"&&node.init.callee.type==="Identifier"&&node.init.callee.name==="require"&&node.init.arguments[0]&&node.init.arguments[0].type==="Literal"&&node.init.arguments[0].value==="child_process"){moduleAliases.add(node.id.name);return}if(node.id.type==="ObjectPattern"&&node.init?.type==="CallExpression"&&node.init.callee.type==="Identifier"&&node.init.callee.name==="require"&&node.init.arguments[0]&&node.init.arguments[0].type==="Literal"&&node.init.arguments[0].value==="child_process"){for(const prop of node.id.properties){if(prop.type==="Property"&&prop.key.type==="Identifier"){importedMethods.add(prop.value.type==="Identifier"?prop.value.name:prop.key.name)}}}};return{CallExpression:checkChildProcessCall,ImportDeclaration:trackChildProcessImport,VariableDeclarator:trackChildProcessRequire}}});
|