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
JavaScript
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 };