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

361 lines (312 loc) 10.4 kB
const crypto = require('crypto'); const path = require('path'); const os = require('os'); const net = require('net'); /** * Input validation utilities for security and data integrity */ class ValidationUtils { /** * Validate and sanitize email addresses * @param {string} email - Email to validate * @returns {string|null} - Sanitized email or null if invalid */ static validateEmail(email) { if (!email || typeof email !== 'string') { return null; } // Trim whitespace first const trimmedEmail = email.trim(); // Additional checks for common invalid patterns if (trimmedEmail.includes('..') || trimmedEmail.includes('.@') || trimmedEmail.includes('@.') || trimmedEmail.endsWith('.')) { return null; } // Basic email regex (RFC 5322 compliant) - stricter version const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; if (!emailRegex.test(trimmedEmail)) { return null; } // Sanitize by converting to lowercase return trimmedEmail.toLowerCase(); } /** * Validate hostname format and security * @param {string} hostname - Hostname to validate * @returns {boolean} - True if valid and secure */ static validateHostname(hostname) { if (!hostname || typeof hostname !== 'string') { return false; } // Check for common attack patterns const dangerousPatterns = [ /[<>"']/, // HTML injection /javascript:/i, // JavaScript injection /data:/i, // Data URI injection /vbscript:/i, // VBScript injection /on\w+\s*=/i, // Event handler injection ]; for (const pattern of dangerousPatterns) { if (pattern.test(hostname)) { return false; } } // Check if it's an IP address (IPv4 or IPv6) if (this.isValidIPAddress(hostname)) { return true; } // Basic hostname format validation const hostnameRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; return hostnameRegex.test(hostname); } /** * Validate port number * @param {string|number} port - Port to validate * @returns {boolean} - True if valid */ static validatePort(port) { // Check if it's a number or numeric string if (typeof port === 'number') { return Number.isInteger(port) && port >= 1 && port <= 65535; } if (typeof port === 'string') { // Must be a whole number if (!/^\d+$/.test(port)) { return false; } const portNum = parseInt(port, 10); return portNum >= 1 && portNum <= 65535; } return false; } /** * Validate file path for security (prevent directory traversal) * @param {string} filePath - File path to validate * @param {string} allowedBase - Base directory that paths must be within * @returns {boolean} - True if path is safe */ static validateFilePath(filePath, allowedBase = null) { if (!filePath || typeof filePath !== 'string') { return false; } // Check for dangerous directory traversal patterns if (filePath.includes('..\\') || filePath.includes('//') || filePath.startsWith('..\\') || filePath.startsWith('//')) { return false; } // Check for excessive directory traversal (more than 2 levels up) if (filePath.includes('..')) { const parts = filePath.split('/'); let upCount = 0; for (const part of parts) { if (part === '..') upCount++; if (upCount > 2) return false; // Limit to 2 levels up } } // Check for dangerous system paths const dangerousSystemPaths = [ /^\/etc\//, /^\/var\//, /^\/usr\//, /^\/bin\//, /^\/sbin\//, /^\/dev\//, /^\/proc\//, /^\/sys\//, /^\/tmp\/\.\./, /^\/var\/tmp\/\.\./, /^C:\\Windows\\System32\\/i, /^file:\/\//, ]; for (const pattern of dangerousSystemPaths) { if (pattern.test(filePath)) { return false; } } // Check for dangerous patterns const dangerousPatterns = [ /^\/$/, // Root directory /^\/etc\//, // System directories /^\/var\//, /^\/usr\//, /^\/bin\//, /^\/sbin\//, /^\/dev\//, /^\/proc\//, /^\/sys\//, /^\/tmp\/\.\./, // Directory traversal from temp /^\/var\/tmp\/\.\./, ]; for (const pattern of dangerousPatterns) { if (pattern.test(filePath)) { return false; } } // Check for absolute paths if base is specified if (allowedBase && path.isAbsolute(filePath)) { const resolvedPath = path.resolve(filePath); const basePath = path.resolve(allowedBase); return resolvedPath.startsWith(basePath); } return true; } /** * Check if string is a valid IP address * @param {string} ip - IP address to validate * @returns {boolean} - True if valid IP */ static isValidIPAddress(ip) { // Use Node.js built-in IP validation return net.isIP(ip) !== 0; } /** * Validate SSH key file for security (permissions, ownership, location) * @param {string} keyPath - Path to SSH key file * @returns {Promise<boolean>} - True if key file is secure */ static async validateSSHKeyFile(keyPath) { try { if (!keyPath || typeof keyPath !== 'string') { return false; } const fs = require('fs').promises; const path = require('path'); const os = require('os'); // Check if file exists and is accessible const stats = await fs.stat(keyPath); // Check if it's a regular file if (!stats.isFile()) { return false; } // Check file size (SSH keys should be reasonable size) const maxKeySize = 64 * 1024; // 64KB max if (stats.size > maxKeySize) { return false; } // Check file permissions (must be 600) const mode = stats.mode & 0o777; if (mode !== 0o600) { return false; } // Check file ownership if (stats.uid !== process.getuid()) { return false; } // Ensure path is within allowed directories const homeDir = process.env.HOME || os.homedir(); const sshDir = path.join(homeDir, '.ssh'); const resolvedPath = path.resolve(keyPath); // Additional security: Check if path contains any suspicious patterns const suspiciousPatterns = [ /\.\./, // Directory traversal /\/tmp\//, // Temporary directories /\/var\/tmp\//, // System temp /\/dev\//, // Device files /\/proc\//, // Process files /\/sys\//, // System files ]; for (const pattern of suspiciousPatterns) { if (pattern.test(resolvedPath)) { return false; } } return resolvedPath.startsWith(sshDir) || resolvedPath.startsWith(homeDir); } catch (error) { return false; } } /** * Sanitize string input to prevent injection attacks * @param {string} input - Input string to sanitize * @returns {string} - Sanitized string */ static sanitizeString(input) { if (!input || typeof input !== 'string') { return ''; } // Remove potentially dangerous characters return input .replace(/[<>"']/g, '') .replace(/javascript:/gi, '') .replace(/data:/gi, '') .replace(/vbscript:/gi, '') .replace(/on\w+\s*=/gi, '') .trim(); } /** * Generate secure random string * @param {number} length - Length of random string * @returns {string} - Random string */ static generateRandomString(length = 32) { return crypto.randomBytes(length).toString('hex'); } /** * Validate API key format * @param {string} apiKey - API key to validate * @returns {boolean} - True if valid format */ static validateAPIKey(apiKey) { if (!apiKey || typeof apiKey !== 'string') { return false; } // API key should be alphanumeric, between 16 and 20 characters if (apiKey.length < 16 || apiKey.length > 20) { return false; } // Only allow alphanumeric characters const apiKeyRegex = /^[a-zA-Z0-9]+$/; return apiKeyRegex.test(apiKey); } /** * Validate command input to prevent command injection * @param {string} command - Command to validate * @returns {boolean} - True if command is safe */ static validateCommand(command) { if (!command || typeof command !== 'string') { return false; } // Check for dangerous command patterns const dangerousPatterns = [ /[;&|`$(){}[\]]/, // Command separators and shell metacharacters /rm\s+-rf/i, // Dangerous rm command /dd\s+if/i, // Dangerous dd command /mkfs/i, // Filesystem creation /fdisk/i, // Disk partitioning /chmod\s+777/i, // Dangerous permissions /chown\s+root/i, // Dangerous ownership changes // SECURITY: Additional dangerous patterns /chmod\s+000/i, // Remove all permissions /chmod\s+-R\s+777/i, // Recursive dangerous permissions /chown\s+-R\s+root/i, // Recursive ownership change /mkfs\s+\/dev/i, // Format specific devices /dd\s+of=\/dev/i, // Write to devices /:\s*\{\s*:\|:&\s*\};\s*:/, // Fork bomb /sudo\s+rm\s+-rf/i, // Sudo with dangerous rm /wget\s+http/i, // Download from HTTP (insecure) /curl\s+http/i, // Curl from HTTP (insecure) /nc\s+-l/i, // Netcat listen mode /ncat\s+-l/i // Ncat listen mode ]; for (const pattern of dangerousPatterns) { if (pattern.test(command)) { return false; } } return true; } /** * Validate version string format (semver) * @param {string} version - Version string to validate * @returns {boolean} - True if valid semver format */ static validateVersion(version) { if (!version || typeof version !== 'string') { return false; } // Semver regex pattern const semverRegex = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/; return semverRegex.test(version); } } module.exports = { ValidationUtils };