@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.
Files changed (34) hide show
  1. package/README.md +234 -0
  2. package/__tests__/acceptance/security-compliance.test.ts +674 -0
  3. package/__tests__/credential-generator.test.ts +310 -0
  4. package/__tests__/fixtures/configurations.ts +419 -0
  5. package/__tests__/fixtures/index.ts +21 -0
  6. package/__tests__/helpers/create-mock.ts +469 -0
  7. package/__tests__/helpers/index.ts +32 -0
  8. package/__tests__/input-validator.test.ts +381 -0
  9. package/__tests__/integration/security-flow.test.ts +606 -0
  10. package/__tests__/password-hasher.test.ts +239 -0
  11. package/__tests__/path-validator.test.ts +302 -0
  12. package/__tests__/safe-executor.test.ts +292 -0
  13. package/__tests__/token-generator.test.ts +371 -0
  14. package/__tests__/unit/credential-generator.test.ts +182 -0
  15. package/__tests__/unit/password-hasher.test.ts +359 -0
  16. package/__tests__/unit/path-validator.test.ts +509 -0
  17. package/__tests__/unit/safe-executor.test.ts +667 -0
  18. package/__tests__/unit/token-generator.test.ts +310 -0
  19. package/package.json +28 -0
  20. package/src/CVE-REMEDIATION.ts +251 -0
  21. package/src/application/index.ts +10 -0
  22. package/src/application/services/security-application-service.ts +193 -0
  23. package/src/credential-generator.ts +368 -0
  24. package/src/domain/entities/security-context.ts +173 -0
  25. package/src/domain/index.ts +17 -0
  26. package/src/domain/services/security-domain-service.ts +296 -0
  27. package/src/index.ts +271 -0
  28. package/src/input-validator.ts +466 -0
  29. package/src/password-hasher.ts +270 -0
  30. package/src/path-validator.ts +525 -0
  31. package/src/safe-executor.ts +525 -0
  32. package/src/token-generator.ts +463 -0
  33. package/tmp.json +0 -0
  34. package/tsconfig.json +9 -0
@@ -0,0 +1,525 @@
1
+ /**
2
+ * Safe Executor - HIGH-1 Remediation
3
+ *
4
+ * Fixes command injection vulnerabilities by:
5
+ * - Using execFile instead of exec with shell
6
+ * - Validating all command arguments
7
+ * - Implementing command allowlist
8
+ * - Sanitizing command inputs
9
+ *
10
+ * Security Properties:
11
+ * - No shell interpretation
12
+ * - Argument validation
13
+ * - Command allowlist enforcement
14
+ * - Timeout controls
15
+ * - Resource limits
16
+ *
17
+ * @module v3/security/safe-executor
18
+ */
19
+
20
+ import { execFile, spawn, ChildProcess } from 'child_process';
21
+ import { promisify } from 'util';
22
+ import * as path from 'path';
23
+
24
+ const execFileAsync = promisify(execFile);
25
+
26
+ export interface ExecutorConfig {
27
+ /**
28
+ * Allowed commands (allowlist).
29
+ * Only commands in this list can be executed.
30
+ */
31
+ allowedCommands: string[];
32
+
33
+ /**
34
+ * Blocked argument patterns (regex strings).
35
+ * Arguments matching these patterns are rejected.
36
+ */
37
+ blockedPatterns?: string[];
38
+
39
+ /**
40
+ * Maximum execution timeout in milliseconds.
41
+ * Default: 30000 (30 seconds)
42
+ */
43
+ timeout?: number;
44
+
45
+ /**
46
+ * Maximum buffer size for stdout/stderr.
47
+ * Default: 10MB
48
+ */
49
+ maxBuffer?: number;
50
+
51
+ /**
52
+ * Working directory for command execution.
53
+ * Default: process.cwd()
54
+ */
55
+ cwd?: string;
56
+
57
+ /**
58
+ * Environment variables to include.
59
+ * Default: process.env
60
+ */
61
+ env?: NodeJS.ProcessEnv;
62
+
63
+ /**
64
+ * Whether to allow sudo commands.
65
+ * Default: false
66
+ */
67
+ allowSudo?: boolean;
68
+ }
69
+
70
+ export interface ExecutionResult {
71
+ stdout: string;
72
+ stderr: string;
73
+ exitCode: number;
74
+ command: string;
75
+ args: string[];
76
+ duration: number;
77
+ }
78
+
79
+ export interface StreamingExecutor {
80
+ process: ChildProcess;
81
+ stdout: NodeJS.ReadableStream | null;
82
+ stderr: NodeJS.ReadableStream | null;
83
+ promise: Promise<ExecutionResult>;
84
+ }
85
+
86
+ export class SafeExecutorError extends Error {
87
+ constructor(
88
+ message: string,
89
+ public readonly code: string,
90
+ public readonly command?: string,
91
+ public readonly args?: string[],
92
+ ) {
93
+ super(message);
94
+ this.name = 'SafeExecutorError';
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Default blocked argument patterns.
100
+ * These patterns indicate potential command injection attempts.
101
+ */
102
+ const DEFAULT_BLOCKED_PATTERNS = [
103
+ // Shell metacharacters
104
+ ';',
105
+ '&&',
106
+ '||',
107
+ '|',
108
+ '`',
109
+ '$(',
110
+ '${',
111
+ // Redirection
112
+ '>',
113
+ '<',
114
+ '>>',
115
+ // Background execution
116
+ '&',
117
+ // Newlines (command chaining)
118
+ '\n',
119
+ '\r',
120
+ // Null byte injection
121
+ '\0',
122
+ // Command substitution
123
+ '$()',
124
+ ];
125
+
126
+ /**
127
+ * Commands that are inherently dangerous and should never be allowed.
128
+ */
129
+ const DANGEROUS_COMMANDS = [
130
+ 'rm',
131
+ 'rmdir',
132
+ 'del',
133
+ 'format',
134
+ 'mkfs',
135
+ 'dd',
136
+ 'chmod',
137
+ 'chown',
138
+ 'kill',
139
+ 'killall',
140
+ 'pkill',
141
+ 'reboot',
142
+ 'shutdown',
143
+ 'init',
144
+ 'poweroff',
145
+ 'halt',
146
+ ];
147
+
148
+ /**
149
+ * Safe command executor that prevents command injection.
150
+ *
151
+ * This class replaces unsafe exec() and spawn({shell: true}) calls
152
+ * with validated execFile() calls.
153
+ *
154
+ * @example
155
+ * ```typescript
156
+ * const executor = new SafeExecutor({
157
+ * allowedCommands: ['git', 'npm', 'node']
158
+ * });
159
+ *
160
+ * const result = await executor.execute('git', ['status']);
161
+ * ```
162
+ */
163
+ export class SafeExecutor {
164
+ private readonly config: Required<ExecutorConfig>;
165
+ private readonly blockedPatterns: RegExp[];
166
+
167
+ constructor(config: ExecutorConfig) {
168
+ this.config = {
169
+ allowedCommands: config.allowedCommands,
170
+ blockedPatterns: config.blockedPatterns ?? DEFAULT_BLOCKED_PATTERNS,
171
+ timeout: config.timeout ?? 30000,
172
+ maxBuffer: config.maxBuffer ?? 10 * 1024 * 1024, // 10MB
173
+ cwd: config.cwd ?? process.cwd(),
174
+ env: config.env ?? process.env,
175
+ allowSudo: config.allowSudo ?? false,
176
+ };
177
+
178
+ // Compile blocked patterns for performance
179
+ this.blockedPatterns = this.config.blockedPatterns.map(
180
+ pattern => new RegExp(this.escapeRegExp(pattern), 'i')
181
+ );
182
+
183
+ this.validateConfig();
184
+ }
185
+
186
+ /**
187
+ * Escapes special regex characters.
188
+ */
189
+ private escapeRegExp(str: string): string {
190
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
191
+ }
192
+
193
+ /**
194
+ * Validates executor configuration.
195
+ */
196
+ private validateConfig(): void {
197
+ if (this.config.allowedCommands.length === 0) {
198
+ throw new SafeExecutorError(
199
+ 'At least one allowed command must be specified',
200
+ 'EMPTY_ALLOWLIST'
201
+ );
202
+ }
203
+
204
+ // Check for dangerous commands in allowlist
205
+ const dangerousAllowed = this.config.allowedCommands.filter(
206
+ cmd => DANGEROUS_COMMANDS.includes(path.basename(cmd))
207
+ );
208
+
209
+ if (dangerousAllowed.length > 0) {
210
+ throw new SafeExecutorError(
211
+ `Dangerous commands cannot be allowed: ${dangerousAllowed.join(', ')}`,
212
+ 'DANGEROUS_COMMAND_ALLOWED'
213
+ );
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Validates a command against the allowlist.
219
+ *
220
+ * @param command - Command to validate
221
+ * @throws SafeExecutorError if command is not allowed
222
+ */
223
+ private validateCommand(command: string): void {
224
+ const basename = path.basename(command);
225
+
226
+ // Check if command is allowed
227
+ const isAllowed = this.config.allowedCommands.some(allowed => {
228
+ const allowedBasename = path.basename(allowed);
229
+ return command === allowed || basename === allowedBasename;
230
+ });
231
+
232
+ if (!isAllowed) {
233
+ throw new SafeExecutorError(
234
+ `Command not in allowlist: ${command}`,
235
+ 'COMMAND_NOT_ALLOWED',
236
+ command
237
+ );
238
+ }
239
+
240
+ // Check for sudo
241
+ if (!this.config.allowSudo && (command === 'sudo' || basename === 'sudo')) {
242
+ throw new SafeExecutorError(
243
+ 'Sudo commands are not allowed',
244
+ 'SUDO_NOT_ALLOWED',
245
+ command
246
+ );
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Validates command arguments for injection patterns.
252
+ *
253
+ * @param args - Arguments to validate
254
+ * @throws SafeExecutorError if arguments contain dangerous patterns
255
+ */
256
+ private validateArguments(args: string[]): void {
257
+ for (const arg of args) {
258
+ // Check for null bytes
259
+ if (arg.includes('\0')) {
260
+ throw new SafeExecutorError(
261
+ 'Null byte detected in argument',
262
+ 'NULL_BYTE_INJECTION',
263
+ undefined,
264
+ args
265
+ );
266
+ }
267
+
268
+ // Check against blocked patterns
269
+ for (const pattern of this.blockedPatterns) {
270
+ if (pattern.test(arg)) {
271
+ throw new SafeExecutorError(
272
+ `Dangerous pattern detected in argument: ${arg}`,
273
+ 'DANGEROUS_PATTERN',
274
+ undefined,
275
+ args
276
+ );
277
+ }
278
+ }
279
+
280
+ // Check for command chaining attempts
281
+ if (/^-.*[;&|]/.test(arg)) {
282
+ throw new SafeExecutorError(
283
+ `Potential command chaining in argument: ${arg}`,
284
+ 'COMMAND_CHAINING',
285
+ undefined,
286
+ args
287
+ );
288
+ }
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Sanitizes a single argument.
294
+ *
295
+ * @param arg - Argument to sanitize
296
+ * @returns Sanitized argument
297
+ */
298
+ sanitizeArgument(arg: string): string {
299
+ // Remove null bytes
300
+ let sanitized = arg.replace(/\0/g, '');
301
+
302
+ // Remove shell metacharacters
303
+ sanitized = sanitized.replace(/[;&|`$(){}><\n\r]/g, '');
304
+
305
+ return sanitized;
306
+ }
307
+
308
+ /**
309
+ * Executes a command safely.
310
+ *
311
+ * @param command - Command to execute (must be in allowlist)
312
+ * @param args - Command arguments
313
+ * @returns Execution result
314
+ * @throws SafeExecutorError on validation failure or execution error
315
+ */
316
+ async execute(command: string, args: string[] = []): Promise<ExecutionResult> {
317
+ const startTime = Date.now();
318
+
319
+ // Validate command
320
+ this.validateCommand(command);
321
+
322
+ // Validate arguments
323
+ this.validateArguments(args);
324
+
325
+ try {
326
+ // Execute command WITHOUT shell
327
+ const { stdout, stderr } = await execFileAsync(command, args, {
328
+ cwd: this.config.cwd,
329
+ env: this.config.env,
330
+ timeout: this.config.timeout,
331
+ maxBuffer: this.config.maxBuffer,
332
+ shell: false, // CRITICAL: Never use shell
333
+ windowsHide: true,
334
+ });
335
+
336
+ return {
337
+ stdout: stdout.toString(),
338
+ stderr: stderr.toString(),
339
+ exitCode: 0,
340
+ command,
341
+ args,
342
+ duration: Date.now() - startTime,
343
+ };
344
+ } catch (error: any) {
345
+ // Handle execution errors
346
+ if (error.killed) {
347
+ throw new SafeExecutorError(
348
+ 'Command execution timed out',
349
+ 'TIMEOUT',
350
+ command,
351
+ args
352
+ );
353
+ }
354
+
355
+ if (error.code === 'ENOENT') {
356
+ throw new SafeExecutorError(
357
+ `Command not found: ${command}`,
358
+ 'COMMAND_NOT_FOUND',
359
+ command,
360
+ args
361
+ );
362
+ }
363
+
364
+ // Return result with non-zero exit code
365
+ return {
366
+ stdout: error.stdout?.toString() ?? '',
367
+ stderr: error.stderr?.toString() ?? error.message,
368
+ exitCode: error.code ?? 1,
369
+ command,
370
+ args,
371
+ duration: Date.now() - startTime,
372
+ };
373
+ }
374
+ }
375
+
376
+ /**
377
+ * Executes a command with streaming output.
378
+ *
379
+ * @param command - Command to execute
380
+ * @param args - Command arguments
381
+ * @returns Streaming executor with process handles
382
+ */
383
+ executeStreaming(command: string, args: string[] = []): StreamingExecutor {
384
+ const startTime = Date.now();
385
+
386
+ // Validate command
387
+ this.validateCommand(command);
388
+
389
+ // Validate arguments
390
+ this.validateArguments(args);
391
+
392
+ // Spawn process WITHOUT shell
393
+ const childProcess = spawn(command, args, {
394
+ cwd: this.config.cwd,
395
+ env: this.config.env,
396
+ timeout: this.config.timeout,
397
+ shell: false, // CRITICAL: Never use shell
398
+ windowsHide: true,
399
+ });
400
+
401
+ const promise = new Promise<ExecutionResult>((resolve, reject) => {
402
+ let stdout = '';
403
+ let stderr = '';
404
+
405
+ childProcess.stdout?.on('data', (data: Buffer) => {
406
+ stdout += data.toString();
407
+ });
408
+
409
+ childProcess.stderr?.on('data', (data: Buffer) => {
410
+ stderr += data.toString();
411
+ });
412
+
413
+ childProcess.on('close', (code) => {
414
+ resolve({
415
+ stdout,
416
+ stderr,
417
+ exitCode: code ?? 0,
418
+ command,
419
+ args,
420
+ duration: Date.now() - startTime,
421
+ });
422
+ });
423
+
424
+ childProcess.on('error', (error) => {
425
+ reject(new SafeExecutorError(
426
+ error.message,
427
+ 'EXECUTION_ERROR',
428
+ command,
429
+ args
430
+ ));
431
+ });
432
+ });
433
+
434
+ return {
435
+ process: childProcess,
436
+ stdout: childProcess.stdout,
437
+ stderr: childProcess.stderr,
438
+ promise,
439
+ };
440
+ }
441
+
442
+ /**
443
+ * Adds a command to the allowlist at runtime.
444
+ *
445
+ * @param command - Command to add
446
+ */
447
+ allowCommand(command: string): void {
448
+ const basename = path.basename(command);
449
+
450
+ if (DANGEROUS_COMMANDS.includes(basename)) {
451
+ throw new SafeExecutorError(
452
+ `Cannot allow dangerous command: ${command}`,
453
+ 'DANGEROUS_COMMAND'
454
+ );
455
+ }
456
+
457
+ if (!this.config.allowedCommands.includes(command)) {
458
+ this.config.allowedCommands.push(command);
459
+ }
460
+ }
461
+
462
+ /**
463
+ * Checks if a command is allowed.
464
+ *
465
+ * @param command - Command to check
466
+ * @returns True if command is allowed
467
+ */
468
+ isCommandAllowed(command: string): boolean {
469
+ const basename = path.basename(command);
470
+ return this.config.allowedCommands.some(allowed => {
471
+ const allowedBasename = path.basename(allowed);
472
+ return command === allowed || basename === allowedBasename;
473
+ });
474
+ }
475
+
476
+ /**
477
+ * Returns the current allowlist.
478
+ */
479
+ getAllowedCommands(): readonly string[] {
480
+ return [...this.config.allowedCommands];
481
+ }
482
+ }
483
+
484
+ /**
485
+ * Factory function to create a safe executor for common development tasks.
486
+ *
487
+ * @returns Configured SafeExecutor for git, npm, and node
488
+ */
489
+ export function createDevelopmentExecutor(): SafeExecutor {
490
+ return new SafeExecutor({
491
+ allowedCommands: [
492
+ 'git',
493
+ 'npm',
494
+ 'npx',
495
+ 'node',
496
+ 'tsc',
497
+ 'vitest',
498
+ 'eslint',
499
+ 'prettier',
500
+ ],
501
+ });
502
+ }
503
+
504
+ /**
505
+ * Factory function to create a read-only executor.
506
+ * Only allows commands that read without modifying.
507
+ *
508
+ * @returns Configured SafeExecutor for read operations
509
+ */
510
+ export function createReadOnlyExecutor(): SafeExecutor {
511
+ return new SafeExecutor({
512
+ allowedCommands: [
513
+ 'git',
514
+ 'cat',
515
+ 'head',
516
+ 'tail',
517
+ 'ls',
518
+ 'find',
519
+ 'grep',
520
+ 'which',
521
+ 'echo',
522
+ ],
523
+ timeout: 10000,
524
+ });
525
+ }