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