@sparkleideas/security 3.0.0-alpha.10
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/README.md +234 -0
- package/__tests__/acceptance/security-compliance.test.ts +674 -0
- package/__tests__/credential-generator.test.ts +310 -0
- package/__tests__/fixtures/configurations.ts +419 -0
- package/__tests__/fixtures/index.ts +21 -0
- package/__tests__/helpers/create-mock.ts +469 -0
- package/__tests__/helpers/index.ts +32 -0
- package/__tests__/input-validator.test.ts +381 -0
- package/__tests__/integration/security-flow.test.ts +606 -0
- package/__tests__/password-hasher.test.ts +239 -0
- package/__tests__/path-validator.test.ts +302 -0
- package/__tests__/safe-executor.test.ts +292 -0
- package/__tests__/token-generator.test.ts +371 -0
- package/__tests__/unit/credential-generator.test.ts +182 -0
- package/__tests__/unit/password-hasher.test.ts +359 -0
- package/__tests__/unit/path-validator.test.ts +509 -0
- package/__tests__/unit/safe-executor.test.ts +667 -0
- package/__tests__/unit/token-generator.test.ts +310 -0
- package/package.json +28 -0
- package/src/CVE-REMEDIATION.ts +251 -0
- package/src/application/index.ts +10 -0
- package/src/application/services/security-application-service.ts +193 -0
- package/src/credential-generator.ts +368 -0
- package/src/domain/entities/security-context.ts +173 -0
- package/src/domain/index.ts +17 -0
- package/src/domain/services/security-domain-service.ts +296 -0
- package/src/index.ts +271 -0
- package/src/input-validator.ts +466 -0
- package/src/password-hasher.ts +270 -0
- package/src/path-validator.ts +525 -0
- package/src/safe-executor.ts +525 -0
- package/src/token-generator.ts +463 -0
- package/tmp.json +0 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* V3 Claude-Flow Safe Executor Unit Tests
|
|
3
|
+
*
|
|
4
|
+
* London School TDD - Behavior Verification
|
|
5
|
+
* Tests secure command execution (CVE-3 prevention)
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
8
|
+
import { createMock, type MockedInterface } from '../../helpers/create-mock';
|
|
9
|
+
import { securityConfigs } from '../../fixtures/configurations';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Safe executor interface (to be implemented)
|
|
13
|
+
*/
|
|
14
|
+
interface ISafeExecutor {
|
|
15
|
+
execute(command: string, args?: string[], options?: ExecuteOptions): Promise<ExecuteResult>;
|
|
16
|
+
isCommandAllowed(command: string): boolean;
|
|
17
|
+
sanitizeArgs(args: string[]): string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Process spawner interface (collaborator)
|
|
22
|
+
*/
|
|
23
|
+
interface IProcessSpawner {
|
|
24
|
+
spawn(command: string, args: string[], options: SpawnOptions): Promise<SpawnResult>;
|
|
25
|
+
kill(pid: number): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Command validator interface (collaborator)
|
|
30
|
+
*/
|
|
31
|
+
interface ICommandValidator {
|
|
32
|
+
extractCommand(input: string): string;
|
|
33
|
+
isBuiltin(command: string): boolean;
|
|
34
|
+
resolveCommand(command: string): Promise<string | null>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface ExecuteOptions {
|
|
38
|
+
timeout?: number;
|
|
39
|
+
cwd?: string;
|
|
40
|
+
env?: Record<string, string>;
|
|
41
|
+
shell?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface ExecuteResult {
|
|
45
|
+
stdout: string;
|
|
46
|
+
stderr: string;
|
|
47
|
+
exitCode: number;
|
|
48
|
+
killed: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface SpawnOptions {
|
|
52
|
+
cwd?: string;
|
|
53
|
+
env?: Record<string, string>;
|
|
54
|
+
timeout?: number;
|
|
55
|
+
shell?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface SpawnResult {
|
|
59
|
+
pid: number;
|
|
60
|
+
stdout: string;
|
|
61
|
+
stderr: string;
|
|
62
|
+
exitCode: number;
|
|
63
|
+
signal?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Safe executor implementation for testing
|
|
68
|
+
*/
|
|
69
|
+
class SafeExecutor implements ISafeExecutor {
|
|
70
|
+
constructor(
|
|
71
|
+
private readonly spawner: IProcessSpawner,
|
|
72
|
+
private readonly validator: ICommandValidator,
|
|
73
|
+
private readonly config: typeof securityConfigs.strict.execution
|
|
74
|
+
) {}
|
|
75
|
+
|
|
76
|
+
async execute(
|
|
77
|
+
command: string,
|
|
78
|
+
args: string[] = [],
|
|
79
|
+
options: ExecuteOptions = {}
|
|
80
|
+
): Promise<ExecuteResult> {
|
|
81
|
+
// Extract base command
|
|
82
|
+
const baseCommand = this.validator.extractCommand(command);
|
|
83
|
+
|
|
84
|
+
// Check if command is allowed
|
|
85
|
+
if (!this.isCommandAllowed(baseCommand)) {
|
|
86
|
+
throw new Error(`Command not allowed: ${baseCommand}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Sanitize arguments
|
|
90
|
+
const sanitizedArgs = this.sanitizeArgs(args);
|
|
91
|
+
|
|
92
|
+
// Apply shell restriction from config
|
|
93
|
+
const spawnOptions: SpawnOptions = {
|
|
94
|
+
cwd: options.cwd,
|
|
95
|
+
env: options.env,
|
|
96
|
+
timeout: options.timeout ?? this.config.timeout,
|
|
97
|
+
shell: this.config.shell, // Always use config, ignore user option
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const result = await this.spawner.spawn(baseCommand, sanitizedArgs, spawnOptions);
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
stdout: result.stdout,
|
|
104
|
+
stderr: result.stderr,
|
|
105
|
+
exitCode: result.exitCode,
|
|
106
|
+
killed: result.signal !== undefined,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
isCommandAllowed(command: string): boolean {
|
|
111
|
+
const baseCommand = this.validator.extractCommand(command);
|
|
112
|
+
|
|
113
|
+
// Check blocked commands first
|
|
114
|
+
if (this.config.blockedCommands.includes(baseCommand)) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Check allowed commands
|
|
119
|
+
return this.config.allowedCommands.includes(baseCommand);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
sanitizeArgs(args: string[]): string[] {
|
|
123
|
+
return args.map(arg => {
|
|
124
|
+
// Remove shell metacharacters
|
|
125
|
+
let sanitized = arg
|
|
126
|
+
.replace(/[;&|`$()]/g, '')
|
|
127
|
+
.replace(/\n/g, '')
|
|
128
|
+
.replace(/\r/g, '');
|
|
129
|
+
|
|
130
|
+
// Remove command substitution
|
|
131
|
+
sanitized = sanitized.replace(/\$\([^)]*\)/g, '');
|
|
132
|
+
sanitized = sanitized.replace(/`[^`]*`/g, '');
|
|
133
|
+
|
|
134
|
+
// Remove redirection
|
|
135
|
+
sanitized = sanitized.replace(/[<>]/g, '');
|
|
136
|
+
|
|
137
|
+
return sanitized;
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
describe('SafeExecutor', () => {
|
|
143
|
+
let mockSpawner: MockedInterface<IProcessSpawner>;
|
|
144
|
+
let mockValidator: MockedInterface<ICommandValidator>;
|
|
145
|
+
let safeExecutor: SafeExecutor;
|
|
146
|
+
const executionConfig = securityConfigs.strict.execution;
|
|
147
|
+
|
|
148
|
+
beforeEach(() => {
|
|
149
|
+
mockSpawner = createMock<IProcessSpawner>();
|
|
150
|
+
mockValidator = createMock<ICommandValidator>();
|
|
151
|
+
|
|
152
|
+
// Configure default mock behavior
|
|
153
|
+
mockValidator.extractCommand.mockImplementation((input: string) => input.split(' ')[0]);
|
|
154
|
+
mockValidator.isBuiltin.mockReturnValue(false);
|
|
155
|
+
mockValidator.resolveCommand.mockResolvedValue('/usr/bin/npm');
|
|
156
|
+
|
|
157
|
+
mockSpawner.spawn.mockResolvedValue({
|
|
158
|
+
pid: 12345,
|
|
159
|
+
stdout: 'command output',
|
|
160
|
+
stderr: '',
|
|
161
|
+
exitCode: 0,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
safeExecutor = new SafeExecutor(mockSpawner, mockValidator, executionConfig);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('execute', () => {
|
|
168
|
+
it('should extract base command before execution', async () => {
|
|
169
|
+
// Given
|
|
170
|
+
const command = 'npm install';
|
|
171
|
+
|
|
172
|
+
// When
|
|
173
|
+
await safeExecutor.execute(command);
|
|
174
|
+
|
|
175
|
+
// Then
|
|
176
|
+
expect(mockValidator.extractCommand).toHaveBeenCalledWith(command);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should spawn process with sanitized arguments', async () => {
|
|
180
|
+
// Given
|
|
181
|
+
const command = 'npm';
|
|
182
|
+
const args = ['install', '--save'];
|
|
183
|
+
|
|
184
|
+
// When
|
|
185
|
+
await safeExecutor.execute(command, args);
|
|
186
|
+
|
|
187
|
+
// Then
|
|
188
|
+
expect(mockSpawner.spawn).toHaveBeenCalledWith(
|
|
189
|
+
'npm',
|
|
190
|
+
['install', '--save'],
|
|
191
|
+
expect.any(Object)
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should use config timeout by default', async () => {
|
|
196
|
+
// Given
|
|
197
|
+
const command = 'npm';
|
|
198
|
+
|
|
199
|
+
// When
|
|
200
|
+
await safeExecutor.execute(command);
|
|
201
|
+
|
|
202
|
+
// Then
|
|
203
|
+
expect(mockSpawner.spawn).toHaveBeenCalledWith(
|
|
204
|
+
'npm',
|
|
205
|
+
[],
|
|
206
|
+
expect.objectContaining({
|
|
207
|
+
timeout: executionConfig.timeout,
|
|
208
|
+
})
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should respect custom timeout option', async () => {
|
|
213
|
+
// Given
|
|
214
|
+
const command = 'npm';
|
|
215
|
+
const customTimeout = 60000;
|
|
216
|
+
|
|
217
|
+
// When
|
|
218
|
+
await safeExecutor.execute(command, [], { timeout: customTimeout });
|
|
219
|
+
|
|
220
|
+
// Then
|
|
221
|
+
expect(mockSpawner.spawn).toHaveBeenCalledWith(
|
|
222
|
+
'npm',
|
|
223
|
+
[],
|
|
224
|
+
expect.objectContaining({
|
|
225
|
+
timeout: customTimeout,
|
|
226
|
+
})
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should always use shell setting from config', async () => {
|
|
231
|
+
// Given
|
|
232
|
+
const command = 'npm';
|
|
233
|
+
|
|
234
|
+
// When
|
|
235
|
+
await safeExecutor.execute(command, [], { shell: true }); // User tries to enable shell
|
|
236
|
+
|
|
237
|
+
// Then
|
|
238
|
+
expect(mockSpawner.spawn).toHaveBeenCalledWith(
|
|
239
|
+
'npm',
|
|
240
|
+
[],
|
|
241
|
+
expect.objectContaining({
|
|
242
|
+
shell: executionConfig.shell, // Should be config value (false)
|
|
243
|
+
})
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('should return execution result', async () => {
|
|
248
|
+
// Given
|
|
249
|
+
const command = 'npm';
|
|
250
|
+
mockSpawner.spawn.mockResolvedValue({
|
|
251
|
+
pid: 12345,
|
|
252
|
+
stdout: 'success output',
|
|
253
|
+
stderr: 'warning',
|
|
254
|
+
exitCode: 0,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// When
|
|
258
|
+
const result = await safeExecutor.execute(command);
|
|
259
|
+
|
|
260
|
+
// Then
|
|
261
|
+
expect(result).toEqual({
|
|
262
|
+
stdout: 'success output',
|
|
263
|
+
stderr: 'warning',
|
|
264
|
+
exitCode: 0,
|
|
265
|
+
killed: false,
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should report killed status when signal present', async () => {
|
|
270
|
+
// Given
|
|
271
|
+
const command = 'npm';
|
|
272
|
+
mockSpawner.spawn.mockResolvedValue({
|
|
273
|
+
pid: 12345,
|
|
274
|
+
stdout: '',
|
|
275
|
+
stderr: 'timeout',
|
|
276
|
+
exitCode: 137,
|
|
277
|
+
signal: 'SIGKILL',
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// When
|
|
281
|
+
const result = await safeExecutor.execute(command);
|
|
282
|
+
|
|
283
|
+
// Then
|
|
284
|
+
expect(result.killed).toBe(true);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('should throw error for blocked command', async () => {
|
|
288
|
+
// Given
|
|
289
|
+
const command = 'rm';
|
|
290
|
+
mockValidator.extractCommand.mockReturnValue('rm');
|
|
291
|
+
|
|
292
|
+
// When/Then
|
|
293
|
+
await expect(safeExecutor.execute(command)).rejects.toThrow(
|
|
294
|
+
'Command not allowed: rm'
|
|
295
|
+
);
|
|
296
|
+
expect(mockSpawner.spawn).not.toHaveBeenCalled();
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('should throw error for command not in allowed list', async () => {
|
|
300
|
+
// Given
|
|
301
|
+
const command = 'wget';
|
|
302
|
+
mockValidator.extractCommand.mockReturnValue('wget');
|
|
303
|
+
|
|
304
|
+
// When/Then
|
|
305
|
+
await expect(safeExecutor.execute(command)).rejects.toThrow(
|
|
306
|
+
'Command not allowed: wget'
|
|
307
|
+
);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe('isCommandAllowed', () => {
|
|
312
|
+
it('should allow npm command', () => {
|
|
313
|
+
// Given
|
|
314
|
+
mockValidator.extractCommand.mockReturnValue('npm');
|
|
315
|
+
|
|
316
|
+
// When
|
|
317
|
+
const result = safeExecutor.isCommandAllowed('npm');
|
|
318
|
+
|
|
319
|
+
// Then
|
|
320
|
+
expect(result).toBe(true);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('should allow npx command', () => {
|
|
324
|
+
// Given
|
|
325
|
+
mockValidator.extractCommand.mockReturnValue('npx');
|
|
326
|
+
|
|
327
|
+
// When
|
|
328
|
+
const result = safeExecutor.isCommandAllowed('npx');
|
|
329
|
+
|
|
330
|
+
// Then
|
|
331
|
+
expect(result).toBe(true);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('should allow node command', () => {
|
|
335
|
+
// Given
|
|
336
|
+
mockValidator.extractCommand.mockReturnValue('node');
|
|
337
|
+
|
|
338
|
+
// When
|
|
339
|
+
const result = safeExecutor.isCommandAllowed('node');
|
|
340
|
+
|
|
341
|
+
// Then
|
|
342
|
+
expect(result).toBe(true);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('should allow git command', () => {
|
|
346
|
+
// Given
|
|
347
|
+
mockValidator.extractCommand.mockReturnValue('git');
|
|
348
|
+
|
|
349
|
+
// When
|
|
350
|
+
const result = safeExecutor.isCommandAllowed('git');
|
|
351
|
+
|
|
352
|
+
// Then
|
|
353
|
+
expect(result).toBe(true);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('should block rm command', () => {
|
|
357
|
+
// Given
|
|
358
|
+
mockValidator.extractCommand.mockReturnValue('rm');
|
|
359
|
+
|
|
360
|
+
// When
|
|
361
|
+
const result = safeExecutor.isCommandAllowed('rm');
|
|
362
|
+
|
|
363
|
+
// Then
|
|
364
|
+
expect(result).toBe(false);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('should block del command', () => {
|
|
368
|
+
// Given
|
|
369
|
+
mockValidator.extractCommand.mockReturnValue('del');
|
|
370
|
+
|
|
371
|
+
// When
|
|
372
|
+
const result = safeExecutor.isCommandAllowed('del');
|
|
373
|
+
|
|
374
|
+
// Then
|
|
375
|
+
expect(result).toBe(false);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('should block format command', () => {
|
|
379
|
+
// Given
|
|
380
|
+
mockValidator.extractCommand.mockReturnValue('format');
|
|
381
|
+
|
|
382
|
+
// When
|
|
383
|
+
const result = safeExecutor.isCommandAllowed('format');
|
|
384
|
+
|
|
385
|
+
// Then
|
|
386
|
+
expect(result).toBe(false);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('should block dd command', () => {
|
|
390
|
+
// Given
|
|
391
|
+
mockValidator.extractCommand.mockReturnValue('dd');
|
|
392
|
+
|
|
393
|
+
// When
|
|
394
|
+
const result = safeExecutor.isCommandAllowed('dd');
|
|
395
|
+
|
|
396
|
+
// Then
|
|
397
|
+
expect(result).toBe(false);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('should block blocked command even if in allowed list', () => {
|
|
401
|
+
// Given - hypothetical scenario where command appears in both lists
|
|
402
|
+
// Blocked should take precedence
|
|
403
|
+
mockValidator.extractCommand.mockReturnValue('rm');
|
|
404
|
+
|
|
405
|
+
// When
|
|
406
|
+
const result = safeExecutor.isCommandAllowed('rm');
|
|
407
|
+
|
|
408
|
+
// Then
|
|
409
|
+
expect(result).toBe(false);
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
describe('sanitizeArgs', () => {
|
|
414
|
+
it('should remove semicolon from arguments', () => {
|
|
415
|
+
// Given
|
|
416
|
+
const args = ['install;rm -rf /'];
|
|
417
|
+
|
|
418
|
+
// When
|
|
419
|
+
const result = safeExecutor.sanitizeArgs(args);
|
|
420
|
+
|
|
421
|
+
// Then
|
|
422
|
+
expect(result[0]).not.toContain(';');
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('should remove pipe operator from arguments', () => {
|
|
426
|
+
// Given
|
|
427
|
+
const args = ['install | cat /etc/passwd'];
|
|
428
|
+
|
|
429
|
+
// When
|
|
430
|
+
const result = safeExecutor.sanitizeArgs(args);
|
|
431
|
+
|
|
432
|
+
// Then
|
|
433
|
+
expect(result[0]).not.toContain('|');
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('should remove ampersand from arguments', () => {
|
|
437
|
+
// Given
|
|
438
|
+
const args = ['install && rm -rf /'];
|
|
439
|
+
|
|
440
|
+
// When
|
|
441
|
+
const result = safeExecutor.sanitizeArgs(args);
|
|
442
|
+
|
|
443
|
+
// Then
|
|
444
|
+
expect(result[0]).not.toContain('&');
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('should remove backticks from arguments', () => {
|
|
448
|
+
// Given
|
|
449
|
+
const args = ['install `rm -rf /`'];
|
|
450
|
+
|
|
451
|
+
// When
|
|
452
|
+
const result = safeExecutor.sanitizeArgs(args);
|
|
453
|
+
|
|
454
|
+
// Then
|
|
455
|
+
expect(result[0]).not.toContain('`');
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('should remove $() command substitution from arguments', () => {
|
|
459
|
+
// Given
|
|
460
|
+
const args = ['install $(rm -rf /)'];
|
|
461
|
+
|
|
462
|
+
// When
|
|
463
|
+
const result = safeExecutor.sanitizeArgs(args);
|
|
464
|
+
|
|
465
|
+
// Then
|
|
466
|
+
expect(result[0]).not.toContain('$(');
|
|
467
|
+
expect(result[0]).not.toContain(')');
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('should remove newlines from arguments', () => {
|
|
471
|
+
// Given
|
|
472
|
+
const args = ['install\nrm -rf /'];
|
|
473
|
+
|
|
474
|
+
// When
|
|
475
|
+
const result = safeExecutor.sanitizeArgs(args);
|
|
476
|
+
|
|
477
|
+
// Then
|
|
478
|
+
expect(result[0]).not.toContain('\n');
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it('should remove redirection operators from arguments', () => {
|
|
482
|
+
// Given
|
|
483
|
+
const args = ['install > /dev/null', 'test < /etc/passwd'];
|
|
484
|
+
|
|
485
|
+
// When
|
|
486
|
+
const result = safeExecutor.sanitizeArgs(args);
|
|
487
|
+
|
|
488
|
+
// Then
|
|
489
|
+
expect(result[0]).not.toContain('>');
|
|
490
|
+
expect(result[1]).not.toContain('<');
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it('should preserve safe argument content', () => {
|
|
494
|
+
// Given
|
|
495
|
+
const args = ['--save-dev', 'lodash@4.17.21', './src/index.ts'];
|
|
496
|
+
|
|
497
|
+
// When
|
|
498
|
+
const result = safeExecutor.sanitizeArgs(args);
|
|
499
|
+
|
|
500
|
+
// Then
|
|
501
|
+
expect(result).toEqual(['--save-dev', 'lodash@4.17.21', './src/index.ts']);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it('should handle empty arguments array', () => {
|
|
505
|
+
// Given
|
|
506
|
+
const args: string[] = [];
|
|
507
|
+
|
|
508
|
+
// When
|
|
509
|
+
const result = safeExecutor.sanitizeArgs(args);
|
|
510
|
+
|
|
511
|
+
// Then
|
|
512
|
+
expect(result).toEqual([]);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it('should sanitize all arguments in array', () => {
|
|
516
|
+
// Given
|
|
517
|
+
const args = ['safe-arg', 'unsafe;arg', 'another|unsafe'];
|
|
518
|
+
|
|
519
|
+
// When
|
|
520
|
+
const result = safeExecutor.sanitizeArgs(args);
|
|
521
|
+
|
|
522
|
+
// Then
|
|
523
|
+
expect(result[0]).toBe('safe-arg');
|
|
524
|
+
expect(result[1]).not.toContain(';');
|
|
525
|
+
expect(result[2]).not.toContain('|');
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
describe('CVE-3 prevention scenarios', () => {
|
|
530
|
+
it('should prevent command injection via arguments', async () => {
|
|
531
|
+
// Given
|
|
532
|
+
const command = 'npm';
|
|
533
|
+
const maliciousArgs = ['install; rm -rf /'];
|
|
534
|
+
|
|
535
|
+
// When
|
|
536
|
+
await safeExecutor.execute(command, maliciousArgs);
|
|
537
|
+
|
|
538
|
+
// Then
|
|
539
|
+
expect(mockSpawner.spawn).toHaveBeenCalledWith(
|
|
540
|
+
'npm',
|
|
541
|
+
['install rm -rf /'], // Semicolon removed
|
|
542
|
+
expect.any(Object)
|
|
543
|
+
);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it('should prevent command substitution attack', async () => {
|
|
547
|
+
// Given
|
|
548
|
+
const command = 'npm';
|
|
549
|
+
const maliciousArgs = ['$(cat /etc/passwd)'];
|
|
550
|
+
|
|
551
|
+
// When
|
|
552
|
+
await safeExecutor.execute(command, maliciousArgs);
|
|
553
|
+
|
|
554
|
+
// Then
|
|
555
|
+
expect(mockSpawner.spawn).toHaveBeenCalledWith(
|
|
556
|
+
'npm',
|
|
557
|
+
['cat /etc/passwd'], // $() removed
|
|
558
|
+
expect.any(Object)
|
|
559
|
+
);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it('should prevent shell expansion attack', async () => {
|
|
563
|
+
// Given
|
|
564
|
+
const command = 'npm';
|
|
565
|
+
const maliciousArgs = ['`whoami`'];
|
|
566
|
+
|
|
567
|
+
// When
|
|
568
|
+
await safeExecutor.execute(command, maliciousArgs);
|
|
569
|
+
|
|
570
|
+
// Then
|
|
571
|
+
expect(mockSpawner.spawn).toHaveBeenCalledWith(
|
|
572
|
+
'npm',
|
|
573
|
+
['whoami'], // Backticks removed
|
|
574
|
+
expect.any(Object)
|
|
575
|
+
);
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
it('should block dangerous commands directly', async () => {
|
|
579
|
+
// Given
|
|
580
|
+
const dangerousCommands = ['rm', 'dd', 'format', 'del'];
|
|
581
|
+
|
|
582
|
+
// When/Then
|
|
583
|
+
for (const cmd of dangerousCommands) {
|
|
584
|
+
mockValidator.extractCommand.mockReturnValue(cmd);
|
|
585
|
+
await expect(safeExecutor.execute(cmd)).rejects.toThrow(
|
|
586
|
+
`Command not allowed: ${cmd}`
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it('should prevent shell mode when disabled in config', async () => {
|
|
592
|
+
// Given
|
|
593
|
+
const command = 'npm';
|
|
594
|
+
const args = ['install && echo pwned'];
|
|
595
|
+
|
|
596
|
+
// When
|
|
597
|
+
await safeExecutor.execute(command, args, { shell: true });
|
|
598
|
+
|
|
599
|
+
// Then
|
|
600
|
+
expect(mockSpawner.spawn).toHaveBeenCalledWith(
|
|
601
|
+
'npm',
|
|
602
|
+
expect.any(Array),
|
|
603
|
+
expect.objectContaining({
|
|
604
|
+
shell: false, // Config overrides user option
|
|
605
|
+
})
|
|
606
|
+
);
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
describe('error handling', () => {
|
|
611
|
+
it('should propagate spawner errors', async () => {
|
|
612
|
+
// Given
|
|
613
|
+
const command = 'npm';
|
|
614
|
+
const spawnError = new Error('Process spawn failed');
|
|
615
|
+
mockSpawner.spawn.mockRejectedValue(spawnError);
|
|
616
|
+
|
|
617
|
+
// When/Then
|
|
618
|
+
await expect(safeExecutor.execute(command)).rejects.toThrow(
|
|
619
|
+
'Process spawn failed'
|
|
620
|
+
);
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it('should handle validator errors', async () => {
|
|
624
|
+
// Given
|
|
625
|
+
const command = 'npm';
|
|
626
|
+
mockValidator.extractCommand.mockImplementation(() => {
|
|
627
|
+
throw new Error('Invalid command format');
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// When/Then
|
|
631
|
+
await expect(safeExecutor.execute(command)).rejects.toThrow(
|
|
632
|
+
'Invalid command format'
|
|
633
|
+
);
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
describe('interaction verification', () => {
|
|
638
|
+
it('should not spawn if command validation fails', async () => {
|
|
639
|
+
// Given
|
|
640
|
+
const command = 'rm';
|
|
641
|
+
mockValidator.extractCommand.mockReturnValue('rm');
|
|
642
|
+
|
|
643
|
+
// When
|
|
644
|
+
try {
|
|
645
|
+
await safeExecutor.execute(command);
|
|
646
|
+
} catch {
|
|
647
|
+
// Expected
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Then
|
|
651
|
+
expect(mockSpawner.spawn).not.toHaveBeenCalled();
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
it('should extract command before checking allowed list', async () => {
|
|
655
|
+
// Given
|
|
656
|
+
const command = 'npm install lodash';
|
|
657
|
+
mockValidator.extractCommand.mockReturnValue('npm');
|
|
658
|
+
|
|
659
|
+
// When
|
|
660
|
+
await safeExecutor.execute(command);
|
|
661
|
+
|
|
662
|
+
// Then
|
|
663
|
+
expect(mockValidator.extractCommand).toHaveBeenCalledWith(command);
|
|
664
|
+
expect(mockValidator.extractCommand).toHaveBeenCalledBefore(mockSpawner.spawn);
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
});
|