UNPKG

ssh-bridge-ai

Version:

AI-Powered SSH Tool with Bulletproof Connections & Enterprise Sandbox Security + Cursor-like Confirmation - Enable AI assistants to securely SSH into your servers with persistent sessions, keepalive, automatic recovery, sandbox command testing, and user c

493 lines (417 loc) 14 kB
const { spawn } = require('child_process'); const { CommandSanitizer } = require('./command-sanitizer'); const { SecureLogger } = require('./secure-logger'); /** * Secure Subprocess Executor * * Implements secure command execution using spawn instead of exec to avoid: * - Shell expansion vulnerabilities * - Command injection attacks * - Path traversal issues */ class SecureExecutor { constructor(options = {}) { this.sanitizer = new CommandSanitizer(options.sanitizer); this.logger = options.logger || new SecureLogger(); this.timeout = options.timeout || 30000; // 30 seconds this.maxOutputSize = options.maxOutputSize || 1024 * 1024; // 1MB this.allowShell = options.allowShell || false; this.workingDirectory = options.workingDirectory || process.cwd(); this.environment = options.environment || process.env; // Track running processes for cleanup this.runningProcesses = new Set(); // Setup process cleanup on exit process.on('exit', () => this.cleanupAll()); process.on('SIGINT', () => this.cleanupAll()); process.on('SIGTERM', () => this.cleanupAll()); } /** * Execute a command securely */ async execute(command, options = {}) { const startTime = Date.now(); try { // Sanitize command const sanitizedCommand = this.sanitizer.sanitizeCommand( command, options.hostname, { applyPolicies: options.applyPolicies !== false } ); // Parse command into executable and arguments const { executable, args } = this.parseCommand(sanitizedCommand); // Validate executable this.validateExecutable(executable, options); // Execute command const result = await this.spawnProcess(executable, args, options); // Log successful execution const duration = Date.now() - startTime; this.logger.logCommand( sanitizedCommand, options.hostname || 'local', options.user || process.env.USER || 'unknown', result.exitCode, duration, { secure: true, executable, args } ); return result; } catch (error) { // Log failed execution const duration = Date.now() - startTime; this.logger.error('Command execution failed', { command: this.logger.sanitize(command), error: error.message, duration, hostname: options.hostname || 'local', user: options.user || process.env.USER || 'unknown' }); throw error; } } /** * Parse command into executable and arguments */ parseCommand(command) { if (typeof command !== 'string') { throw new Error('Command must be a string'); } // Split command into parts const parts = command.trim().split(/\s+/); if (parts.length === 0) { throw new Error('Empty command'); } const executable = parts[0]; const args = parts.slice(1); return { executable, args }; } /** * Validate executable for security */ validateExecutable(executable, options = {}) { // Check for path traversal attempts if (executable.includes('..') || executable.includes('//')) { throw new Error('Executable path contains suspicious patterns'); } // Check for absolute paths if not allowed if (!options.allowAbsolutePaths && executable.startsWith('/')) { throw new Error('Absolute paths not allowed for security'); } // Check for dangerous executables const dangerousExecutables = [ 'rm', 'dd', 'mkfs', 'fdisk', 'parted', 'shred', 'nc', 'netcat', 'python', 'perl', 'ruby', 'php' ]; if (dangerousExecutables.includes(executable) && !options.allowDangerousExecutables) { throw new Error(`Executable '${executable}' is considered dangerous and not allowed`); } } /** * Spawn process securely */ async spawnProcess(executable, args, options = {}) { return new Promise((resolve, reject) => { // Create process const process = spawn(executable, args, { cwd: options.workingDirectory || this.workingDirectory, env: options.environment || this.environment, stdio: options.stdio || 'pipe', shell: this.allowShell && options.allowShell, windowsHide: true }); // Track process this.runningProcesses.add(process); // Setup timeout const timeoutId = setTimeout(() => { this.killProcess(process); reject(new Error(`Command execution timed out after ${this.timeout}ms`)); }, options.timeout || this.timeout); // Collect output let stdout = ''; let stderr = ''; if (process.stdout) { process.stdout.on('data', (data) => { stdout += data.toString(); // Check output size limit if (stdout.length > this.maxOutputSize) { this.killProcess(process); clearTimeout(timeoutId); reject(new Error('Command output exceeded maximum size limit')); } }); } if (process.stderr) { process.stderr.on('data', (data) => { stderr += data.toString(); // Check output size limit if (stderr.length > this.maxOutputSize) { this.killProcess(process); clearTimeout(timeoutId); reject(new Error('Command error output exceeded maximum size limit')); } }); } // Handle process completion process.on('close', (code) => { clearTimeout(timeoutId); this.runningProcesses.delete(process); resolve({ exitCode: code, stdout: this.sanitizeOutput(stdout), stderr: this.sanitizeOutput(stderr), success: code === 0 }); }); // Handle process errors process.on('error', (error) => { clearTimeout(timeoutId); this.runningProcesses.delete(process); reject(new Error(`Process error: ${error.message}`)); }); // Handle process exit process.on('exit', (code, signal) => { clearTimeout(timeoutId); this.runningProcesses.delete(process); if (signal) { reject(new Error(`Process killed by signal: ${signal}`)); } }); }); } /** * Execute command with input */ async executeWithInput(command, input, options = {}) { const startTime = Date.now(); try { // Sanitize command const sanitizedCommand = this.sanitizer.sanitizeCommand( command, options.hostname, { applyPolicies: options.applyPolicies !== false } ); // Parse command const { executable, args } = this.parseCommand(sanitizedCommand); // Validate executable this.validateExecutable(executable, options); // Execute with input const result = await this.spawnProcessWithInput(executable, args, input, options); // Log successful execution const duration = Date.now() - startTime; this.logger.logCommand( sanitizedCommand, options.hostname || 'local', options.user || process.env.USER || 'unknown', result.exitCode, duration, { secure: true, executable, args, hasInput: true } ); return result; } catch (error) { // Log failed execution const duration = Date.now() - startTime; this.logger.error('Command execution with input failed', { command: this.logger.sanitize(command), error: error.message, duration, hostname: options.hostname || 'local', user: options.user || process.env.USER || 'unknown' }); throw error; } } /** * Spawn process with input */ async spawnProcessWithInput(executable, args, input, options = {}) { return new Promise((resolve, reject) => { // Create process const process = spawn(executable, args, { cwd: options.workingDirectory || this.workingDirectory, env: options.environment || this.environment, stdio: ['pipe', 'pipe', 'pipe'], shell: this.allowShell && options.allowShell, windowsHide: true }); // Track process this.runningProcesses.add(process); // Setup timeout const timeoutId = setTimeout(() => { this.killProcess(process); reject(new Error(`Command execution timed out after ${this.timeout}ms`)); }, options.timeout || this.timeout); // Collect output let stdout = ''; let stderr = ''; process.stdout.on('data', (data) => { stdout += data.toString(); if (stdout.length > this.maxOutputSize) { this.killProcess(process); clearTimeout(timeoutId); reject(new Error('Command output exceeded maximum size limit')); } }); process.stderr.on('data', (data) => { stderr += data.toString(); if (stderr.length > this.maxOutputSize) { this.killProcess(process); clearTimeout(timeoutId); reject(new Error('Command error output exceeded maximum size limit')); } }); // Write input if (input && process.stdin) { process.stdin.write(input); process.stdin.end(); } // Handle process completion process.on('close', (code) => { clearTimeout(timeoutId); this.runningProcesses.delete(process); resolve({ exitCode: code, stdout: this.sanitizeOutput(stdout), stderr: this.sanitizeOutput(stderr), success: code === 0 }); }); // Handle process errors process.on('error', (error) => { clearTimeout(timeoutId); this.runningProcesses.delete(process); reject(new Error(`Process error: ${error.message}`)); }); }); } /** * Execute command in background */ async executeBackground(command, options = {}) { try { // Sanitize command const sanitizedCommand = this.sanitizer.sanitizeCommand( command, options.hostname, { applyPolicies: options.applyPolicies !== false } ); // Parse command const { executable, args } = this.parseCommand(sanitizedCommand); // Validate executable this.validateExecutable(executable, options); // Spawn background process const process = spawn(executable, args, { cwd: options.workingDirectory || this.workingDirectory, env: options.environment || this.environment, stdio: 'ignore', shell: this.allowShell && options.allowShell, windowsHide: true, detached: true }); // Track process this.runningProcesses.add(process); // Log background execution this.logger.info('Background command started', { command: this.logger.sanitize(sanitizedCommand), pid: process.pid, hostname: options.hostname || 'local', user: options.user || (options.environment && options.environment.USER) || 'unknown' }); return { pid: process.pid, process: process }; } catch (error) { this.logger.error('Background command execution failed', { command: this.logger.sanitize(command), error: error.message, hostname: options.hostname || 'local', user: options.user || (options.environment && options.environment.USER) || 'unknown' }); throw error; } } /** * Kill a process */ killProcess(process) { try { if (process && !process.killed) { process.kill('SIGTERM'); // Force kill after a short delay if needed setTimeout(() => { if (process && !process.killed) { process.kill('SIGKILL'); } }, 1000); } } catch (error) { // Ignore errors when killing processes } } /** * Cleanup all running processes */ cleanupAll() { for (const process of this.runningProcesses) { this.killProcess(process); } this.runningProcesses.clear(); } /** * Sanitize output for logging */ sanitizeOutput(output) { if (!output) return output; // Limit output length for logging const maxLogLength = 1000; if (output.length > maxLogLength) { return output.substring(0, maxLogLength) + '... [truncated]'; } return output; } /** * Get list of running processes */ getRunningProcesses() { return Array.from(this.runningProcesses).map(process => ({ pid: process.pid, killed: process.killed })); } /** * Update configuration */ updateConfig(newConfig) { if (newConfig.timeout) { this.timeout = newConfig.timeout; } if (newConfig.maxOutputSize) { this.maxOutputSize = newConfig.maxOutputSize; } if (newConfig.allowShell !== undefined) { this.allowShell = newConfig.allowShell; } if (newConfig.workingDirectory) { this.workingDirectory = newConfig.workingDirectory; } if (newConfig.environment) { this.environment = newConfig.environment; } // Update sanitizer config if (newConfig.sanitizer) { this.sanitizer.updateConfig(newConfig.sanitizer); } } /** * Get current configuration */ getConfig() { return { timeout: this.timeout, maxOutputSize: this.maxOutputSize, allowShell: this.allowShell, workingDirectory: this.workingDirectory, runningProcesses: this.runningProcesses.size, sanitizer: this.sanitizer.getConfig() }; } } module.exports = { SecureExecutor };