/** * HVAC Testing Framework - Secure Command Execution * * Provides secure command execution with input validation, parameterization, * and command allowlisting to prevent injection vulnerabilities. * * Security Features: * - Command allowlisting with parameter validation * - Input sanitization and validation * - Subprocess execution with proper isolation * - Audit logging of all command executions * - Timeout and resource limits * * @author Claude Code - Emergency Security Response * @version 1.0.0 * @security CRITICAL - Prevents command injection vulnerabilities */ const { spawn, execFile } = require('child_process'); const path = require('path'); const { getCredentialManager } = require('./SecureCredentialManager'); class SecureCommandExecutor { constructor() { this.credentialManager = getCredentialManager(); this.allowedCommands = this.initializeCommandAllowlist(); this.executionLog = []; } /** * Initialize allowlist of safe commands with their parameter patterns * @returns {Map} Command allowlist configuration */ initializeCommandAllowlist() { return new Map([ // WordPress CLI commands ['wp', { binary: 'wp', allowedSubcommands: [ 'rewrite flush', 'eval', 'user list', 'user get', 'option get', 'option update', 'db query', 'plugin list', 'theme list' ], parameterValidation: { 'eval': /^['"][^'";&|`$()]*['"]$/, 'user get': /^[a-zA-Z0-9@._-]+$/, 'option get': /^[a-zA-Z0-9_-]+$/, 'option update': /^[a-zA-Z0-9_-]+\s+['"][^'";&|`$()]*['"]$/, 'db query': /^['"]SELECT\s+[^'";&|`$()]*['"]$/ }, timeout: 30000, maxOutputSize: 1024 * 1024 // 1MB }], // Node.js and npm commands ['node', { binary: 'node', allowedSubcommands: [ '--version', '-e' ], parameterValidation: { '-e': /^['"]console\.log\([^'";&|`$()]*\)['"]$/ }, timeout: 10000, maxOutputSize: 64 * 1024 // 64KB }], // Playwright commands ['npx', { binary: 'npx', allowedSubcommands: [ 'playwright install', 'playwright test' ], parameterValidation: { 'playwright test': /^[a-zA-Z0-9/_.-]+\.js$/ }, timeout: 120000, maxOutputSize: 10 * 1024 * 1024 // 10MB }], // System information commands (read-only) ['ls', { binary: 'ls', allowedSubcommands: ['-la', '-l', ''], parameterValidation: { '': /^[a-zA-Z0-9/_.-]+$/, '-l': /^[a-zA-Z0-9/_.-]+$/, '-la': /^[a-zA-Z0-9/_.-]+$/ }, timeout: 5000, maxOutputSize: 64 * 1024 }], ['cat', { binary: 'cat', allowedSubcommands: [''], parameterValidation: { '': /^[a-zA-Z0-9/_.-]+\.(log|txt|json)$/ }, timeout: 5000, maxOutputSize: 1024 * 1024 }] ]); } /** * Execute a command securely with validation and isolation * @param {string} command - Command to execute * @param {Array} args - Command arguments * @param {Object} options - Execution options * @returns {Promise} Execution result */ async executeCommand(command, args = [], options = {}) { // Validate command is in allowlist const commandConfig = this.allowedCommands.get(command); if (!commandConfig) { throw new Error(`Command not allowed: ${command}`); } // Validate subcommand and parameters await this.validateCommandParameters(command, args, commandConfig); // Prepare execution environment const executionOptions = this.prepareExecutionOptions(commandConfig, options); // Log command execution attempt const executionId = this.logCommandExecution(command, args, options); try { // Execute command with isolation const result = await this.executeWithIsolation( commandConfig.binary, args, executionOptions ); // Log successful execution this.logCommandResult(executionId, 'SUCCESS', result); return result; } catch (error) { // Log failed execution this.logCommandResult(executionId, 'FAILED', { error: error.message }); throw error; } } /** * Validate command parameters against allowlist patterns * @param {string} command - Base command * @param {Array} args - Command arguments * @param {Object} commandConfig - Command configuration */ async validateCommandParameters(command, args, commandConfig) { if (args.length === 0) return; const subcommand = args[0]; const fullSubcommand = args.join(' '); // Check if subcommand is allowed const isAllowed = commandConfig.allowedSubcommands.some(allowed => { if (allowed === '') return true; if (allowed === subcommand) return true; if (fullSubcommand.startsWith(allowed)) return true; return false; }); if (!isAllowed) { throw new Error(`Subcommand not allowed: ${command} ${fullSubcommand}`); } // Validate parameters against patterns if (commandConfig.parameterValidation) { for (const [pattern, regex] of Object.entries(commandConfig.parameterValidation)) { if (fullSubcommand.startsWith(pattern)) { const parameterPart = fullSubcommand.substring(pattern.length).trim(); if (parameterPart && !regex.test(parameterPart)) { throw new Error(`Invalid parameters for ${command} ${pattern}: ${parameterPart}`); } } } } // Additional security checks await this.performSecurityChecks(args); } /** * Perform additional security checks on command arguments * @param {Array} args - Command arguments */ async performSecurityChecks(args) { const dangerousPatterns = [ /[;&|`$(){}]/, // Shell metacharacters /\.\./, // Directory traversal /\/etc\/passwd/, // System file access /rm\s+-rf/, // Destructive commands /sudo/, // Privilege escalation /chmod/, // Permission changes /curl.*http/, // External network access /wget.*http/ // External downloads ]; const fullCommand = args.join(' '); for (const pattern of dangerousPatterns) { if (pattern.test(fullCommand)) { throw new Error(`Security violation: dangerous pattern detected in command`); } } } /** * Prepare secure execution options * @param {Object} commandConfig - Command configuration * @param {Object} userOptions - User-provided options * @returns {Object} Execution options */ prepareExecutionOptions(commandConfig, userOptions) { return { timeout: commandConfig.timeout, maxBuffer: commandConfig.maxOutputSize, env: this.createSecureEnvironment(), shell: false, // Disable shell to prevent injection stdio: ['pipe', 'pipe', 'pipe'], ...userOptions }; } /** * Create secure execution environment * @returns {Object} Environment variables */ createSecureEnvironment() { // Start with minimal environment const secureEnv = { PATH: '/usr/local/bin:/usr/bin:/bin', NODE_ENV: process.env.NODE_ENV || 'development', HOME: process.env.HOME }; // Add WordPress CLI path if needed if (process.env.WP_CLI_PATH) { secureEnv.WP_CLI_PATH = process.env.WP_CLI_PATH; } return secureEnv; } /** * Execute command with proper isolation * @param {string} binary - Command binary * @param {Array} args - Arguments * @param {Object} options - Execution options * @returns {Promise} Execution result */ async executeWithIsolation(binary, args, options) { return new Promise((resolve, reject) => { const startTime = Date.now(); let stdout = ''; let stderr = ''; const child = spawn(binary, args, options); // Set up timeout const timeoutId = setTimeout(() => { child.kill('SIGTERM'); reject(new Error(`Command timeout after ${options.timeout}ms`)); }, options.timeout); // Collect output with size limits child.stdout.on('data', (data) => { stdout += data.toString(); if (stdout.length > options.maxBuffer) { child.kill('SIGTERM'); reject(new Error('Output size limit exceeded')); } }); child.stderr.on('data', (data) => { stderr += data.toString(); if (stderr.length > options.maxBuffer) { child.kill('SIGTERM'); reject(new Error('Error output size limit exceeded')); } }); child.on('close', (code) => { clearTimeout(timeoutId); const duration = Date.now() - startTime; if (code === 0) { resolve({ stdout: stdout.trim(), stderr: stderr.trim(), exitCode: code, duration }); } else { reject(new Error(`Command failed with exit code ${code}: ${stderr}`)); } }); child.on('error', (error) => { clearTimeout(timeoutId); reject(error); }); }); } /** * Log command execution for audit trail * @param {string} command - Command name * @param {Array} args - Arguments * @param {Object} options - Options * @returns {string} Execution ID */ logCommandExecution(command, args, options) { const executionId = require('crypto').randomUUID(); const logEntry = { executionId, timestamp: new Date().toISOString(), command, args: args.map(arg => this.sanitizeForLogging(arg)), options: this.sanitizeOptionsForLogging(options), status: 'STARTED' }; this.executionLog.push(logEntry); this.credentialManager.auditLogger('COMMAND_EXECUTION_STARTED', logEntry); return executionId; } /** * Log command execution result * @param {string} executionId - Execution ID * @param {string} status - Execution status * @param {Object} result - Execution result */ logCommandResult(executionId, status, result) { const logEntry = { executionId, timestamp: new Date().toISOString(), status, duration: result.duration, exitCode: result.exitCode, outputSize: result.stdout ? result.stdout.length : 0, errorSize: result.stderr ? result.stderr.length : 0 }; this.executionLog.push(logEntry); this.credentialManager.auditLogger('COMMAND_EXECUTION_COMPLETED', logEntry); } /** * Sanitize arguments for logging (remove sensitive data) * @param {string} arg - Argument to sanitize * @returns {string} Sanitized argument */ sanitizeForLogging(arg) { // Remove potential passwords and sensitive data return arg.replace(/password[=:]\s*[^\s]+/gi, 'password=***') .replace(/token[=:]\s*[^\s]+/gi, 'token=***') .replace(/key[=:]\s*[^\s]+/gi, 'key=***'); } /** * Sanitize execution options for logging * @param {Object} options - Options to sanitize * @returns {Object} Sanitized options */ sanitizeOptionsForLogging(options) { const sanitized = { ...options }; if (sanitized.env) { sanitized.env = Object.keys(sanitized.env); } return sanitized; } /** * WordPress-specific secure command execution * @param {string} wpCommand - WP-CLI command * @param {Array} args - Additional arguments * @returns {Promise} Command result */ async executeWordPressCommand(wpCommand, args = []) { const wpArgs = wpCommand.split(' ').concat(args); return await this.executeCommand('wp', wpArgs); } /** * Get execution audit log * @returns {Array} Complete execution log */ getExecutionLog() { return [...this.executionLog]; } /** * Clear execution log (for cleanup) */ clearExecutionLog() { this.executionLog = []; } } // Singleton instance let commandExecutorInstance = null; /** * Get singleton instance of SecureCommandExecutor * @returns {SecureCommandExecutor} */ function getCommandExecutor() { if (!commandExecutorInstance) { commandExecutorInstance = new SecureCommandExecutor(); } return commandExecutorInstance; } module.exports = { SecureCommandExecutor, getCommandExecutor };