UNPKG

claude-computer-use-mcp

Version:

MCP server providing browser automation capabilities to Claude Code

167 lines 6.52 kB
import { DEFAULT_SECURITY_CONFIG } from './types.js'; // Performance optimization: cache compiled regex patterns const DANGEROUS_SCRIPT_PATTERNS = [ /require\s*\(/i, /import\s+/i, /eval\s*\(/i, /Function\s*\(/i, /setTimeout\s*\(/i, /setInterval\s*\(/i, /XMLHttpRequest/i, /fetch\s*\(/i, /\.constructor\s*\(/i, /__proto__/i, /process\./i, /child_process/i, /fs\./i, /readFile/i, /writeFile/i, ]; // Cached regex for session ID validation const SESSION_ID_PATTERN = /^session-[a-f0-9]{32}$/; export class ValidationError extends Error { constructor(message) { super(message); this.name = 'ValidationError'; } } export class SecurityError extends Error { constructor(message) { super(message); this.name = 'SecurityError'; } } export function validateUrl(url, config = DEFAULT_SECURITY_CONFIG) { if (!url || typeof url !== 'string') { throw new ValidationError('URL must be a non-empty string'); } let parsedUrl; try { parsedUrl = new URL(url); } catch (error) { throw new ValidationError(`Invalid URL format: ${url}`); } if (!config.allowedProtocols.includes(parsedUrl.protocol)) { throw new SecurityError(`Protocol '${parsedUrl.protocol}' not allowed. Allowed protocols: ${config.allowedProtocols.join(', ')}`); } // Prevent navigation to local files if (parsedUrl.protocol === 'file:') { throw new SecurityError('Navigation to local files is not allowed'); } // Prevent navigation to internal IPs and loopback addresses const hostname = parsedUrl.hostname; // Check for localhost variations if (hostname === 'localhost' || hostname.endsWith('.localhost')) { throw new SecurityError('Navigation to localhost is not allowed'); } // Check if hostname is an IP address const ipv4Pattern = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/; const match = hostname.match(ipv4Pattern); if (match) { const [, a, b, c, d] = match.map(Number); // Validate IP octets if ([a, b, c, d].some(octet => octet > 255)) { throw new ValidationError('Invalid IP address'); } // Check for loopback (127.0.0.0/8) if (a === 127) { throw new SecurityError('Navigation to loopback addresses is not allowed'); } // Check for private IP ranges (RFC1918) // 10.0.0.0/8 if (a === 10) { throw new SecurityError('Navigation to private IP addresses (10.0.0.0/8) is not allowed'); } // 172.16.0.0/12 (172.16.0.0 - 172.31.255.255) if (a === 172 && b >= 16 && b <= 31) { throw new SecurityError('Navigation to private IP addresses (172.16.0.0/12) is not allowed'); } // 192.168.0.0/16 if (a === 192 && b === 168) { throw new SecurityError('Navigation to private IP addresses (192.168.0.0/16) is not allowed'); } // Link-local addresses (169.254.0.0/16) if (a === 169 && b === 254) { throw new SecurityError('Navigation to link-local addresses is not allowed'); } } // Check for IPv6 loopback and private addresses if (hostname === '::1' || hostname.startsWith('fe80:') || hostname.startsWith('fc00:') || hostname.startsWith('fd00:')) { throw new SecurityError('Navigation to private IPv6 addresses is not allowed'); } } export function validateSelector(selector, config = DEFAULT_SECURITY_CONFIG) { if (!selector || typeof selector !== 'string') { throw new ValidationError('Selector must be a non-empty string'); } if (selector.length > config.maxSelectorLength) { throw new ValidationError(`Selector too long (max ${config.maxSelectorLength} characters)`); } // Basic XSS prevention if (selector.includes('<script') || selector.includes('javascript:')) { throw new SecurityError('Selector contains potentially malicious content'); } } export function validateText(text, config = DEFAULT_SECURITY_CONFIG) { if (typeof text !== 'string') { throw new ValidationError('Text must be a string'); } if (text.length > config.maxTextLength) { throw new ValidationError(`Text too long (max ${config.maxTextLength} characters)`); } } export function validateScript(script, config = DEFAULT_SECURITY_CONFIG) { if (!config.allowJavaScriptExecution) { throw new SecurityError('JavaScript execution is disabled for security reasons'); } if (!script || typeof script !== 'string') { throw new ValidationError('Script must be a non-empty string'); } if (script.length > config.maxScriptLength) { throw new ValidationError(`Script too long (max ${config.maxScriptLength} characters)`); } // Basic security checks - these are not comprehensive but catch obvious issues for (const pattern of DANGEROUS_SCRIPT_PATTERNS) { if (pattern.test(script)) { throw new SecurityError(`Script contains potentially dangerous pattern: ${pattern}`); } } } export function validateSessionId(sessionId) { if (!sessionId || typeof sessionId !== 'string') { throw new ValidationError('Session ID must be a non-empty string'); } // Session IDs should match our expected format (session-[32 hex chars]) if (!SESSION_ID_PATTERN.test(sessionId)) { throw new ValidationError('Invalid session ID format'); } } export function validateTimeout(timeout) { if (timeout === undefined) { return 30000; // Default 30 seconds } if (typeof timeout !== 'number' || isNaN(timeout)) { throw new ValidationError('Timeout must be a number'); } if (timeout < 0) { throw new ValidationError('Timeout must be non-negative'); } if (timeout > 300000) { // Max 5 minutes throw new ValidationError('Timeout too large (max 5 minutes)'); } return timeout; } export function validateAttribute(attribute) { if (!attribute || typeof attribute !== 'string') { throw new ValidationError('Attribute must be a non-empty string'); } if (attribute.length > 100) { throw new ValidationError('Attribute name too long'); } // Only allow alphanumeric, dash, and data- attributes if (!/^[a-zA-Z0-9-]+$/.test(attribute)) { throw new ValidationError('Invalid attribute name'); } } //# sourceMappingURL=validation.js.map