UNPKG

@albert-mr/genlayer-mcp-server

Version:

MCP server for generating GenLayer Intelligent Contracts with AI-native blockchain capabilities

304 lines 12.1 kB
// Security utilities for input validation and sanitization import { VALIDATION_PATTERNS } from '../config/constants.js'; export class SecurityValidator { /** * Sanitize and validate URLs to prevent malicious injections */ static validateUrl(url) { try { // Basic validation if (!url || typeof url !== 'string') { return { isValid: false, error: 'URL must be a non-empty string' }; } // Remove potential whitespace and control characters const trimmedUrl = url.trim().replace(/[\x00-\x1F\x7F]/g, ''); // Check for basic URL format if (!VALIDATION_PATTERNS.validUrl.test(trimmedUrl)) { return { isValid: false, error: 'Invalid URL format' }; } // Parse URL to validate components const urlObj = new URL(trimmedUrl); // Security checks const allowedProtocols = ['http:', 'https:']; if (!allowedProtocols.includes(urlObj.protocol)) { return { isValid: false, error: 'Only HTTP and HTTPS protocols are allowed' }; } // Check for potential malicious patterns const maliciousPatterns = [ /javascript:/i, /data:/i, /vbscript:/i, /file:/i, /ftp:/i, /<script/i, /onload=/i, /onerror=/i ]; for (const pattern of maliciousPatterns) { if (pattern.test(trimmedUrl)) { return { isValid: false, error: 'URL contains potentially malicious content' }; } } // Validate hostname (no localhost in production-like contexts) const suspiciousHosts = ['localhost', '127.0.0.1', '0.0.0.0', '::1']; if (process.env.NODE_ENV === 'production' && suspiciousHosts.includes(urlObj.hostname)) { return { isValid: false, error: 'Localhost URLs not allowed in production' }; } return { isValid: true, sanitizedUrl: urlObj.toString() }; } catch (error) { return { isValid: false, error: `Invalid URL: ${error instanceof Error ? error.message : 'Unknown error'}` }; } } /** * Sanitize contract code to prevent code injection */ static sanitizeContractCode(code) { if (!code || typeof code !== 'string') { return { sanitizedCode: '', warnings: ['Empty or invalid code provided'] }; } const warnings = []; let sanitizedCode = code; // Remove potentially dangerous imports/statements const dangerousPatterns = [ { pattern: /import\s+os/gi, warning: 'Removed OS import (potentially dangerous)' }, { pattern: /import\s+subprocess/gi, warning: 'Removed subprocess import (potentially dangerous)' }, { pattern: /import\s+sys/gi, warning: 'Removed sys import (potentially dangerous)' }, { pattern: /exec\s*\(/gi, warning: 'Removed exec() call (potentially dangerous)' }, { pattern: /eval\s*\(/gi, warning: 'Removed eval() call (potentially dangerous)' }, { pattern: /__import__\s*\(/gi, warning: 'Removed __import__() call (potentially dangerous)' }, { pattern: /open\s*\(/gi, warning: 'Removed file open() call (potentially dangerous)' }, { pattern: /file\s*\(/gi, warning: 'Removed file() call (potentially dangerous)' } ]; for (const { pattern, warning } of dangerousPatterns) { if (pattern.test(sanitizedCode)) { sanitizedCode = sanitizedCode.replace(pattern, '# REMOVED FOR SECURITY'); warnings.push(warning); } } // Check for potential shell injection attempts const shellPatterns = [';', '|', '&', '$', '`', '$(']; for (const pattern of shellPatterns) { if (sanitizedCode.includes(pattern)) { warnings.push(`Potential shell metacharacter detected: ${pattern}`); } } return { sanitizedCode, warnings }; } /** * Validate contract names and identifiers */ static validateIdentifier(identifier, type) { if (!identifier || typeof identifier !== 'string') { return { isValid: false, error: `${type} name must be a non-empty string` }; } // Remove potential whitespace const trimmed = identifier.trim(); if (trimmed.length === 0) { return { isValid: false, error: `${type} name cannot be empty` }; } // Check length limits if (trimmed.length > 100) { return { isValid: false, error: `${type} name too long (max 100 characters)` }; } // Check for appropriate pattern based on type let pattern; switch (type) { case 'contract': pattern = VALIDATION_PATTERNS.pascalCase; break; case 'method': pattern = VALIDATION_PATTERNS.methodName; break; case 'variable': pattern = VALIDATION_PATTERNS.camelCase; break; default: return { isValid: false, error: 'Unknown identifier type' }; } if (!pattern.test(trimmed)) { return { isValid: false, error: `${type} name must follow proper naming convention` }; } // Check for reserved words and dangerous names const reservedWords = [ 'eval', 'exec', 'import', 'open', 'file', 'input', 'raw_input', 'compile', '__import__', 'globals', 'locals', 'vars', 'dir', 'hasattr', 'getattr', 'setattr', 'delattr', 'callable' ]; if (reservedWords.includes(trimmed.toLowerCase())) { return { isValid: false, error: `${type} name cannot be a reserved or dangerous word` }; } return { isValid: true }; } /** * Sanitize text input to prevent XSS and injection */ static sanitizeTextInput(input, maxLength = 10000) { if (!input || typeof input !== 'string') { return { sanitizedText: '', warnings: ['Empty or invalid input provided'] }; } const warnings = []; let sanitizedText = input; // Check length if (sanitizedText.length > maxLength) { sanitizedText = sanitizedText.substring(0, maxLength); warnings.push(`Input truncated to ${maxLength} characters`); } // Remove control characters except newlines and tabs sanitizedText = sanitizedText.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ''); // Check for potential script injection const scriptPatterns = [ /<script/gi, /javascript:/gi, /onload=/gi, /onerror=/gi, /onclick=/gi, /onmouseover=/gi ]; for (const pattern of scriptPatterns) { if (pattern.test(sanitizedText)) { warnings.push('Potential script injection detected in input'); break; } } // Check for SQL injection patterns const sqlPatterns = [ /('|(\\'))+.*(--|\#)/gi, /(('|(\\'))+.*(\-\-|\#))/gi, /\w*\s*((\=)|(\:))\s*('|(\\'))+((\s*((\-\-)|(\#)))|(('|(\\'))+))/gi ]; for (const pattern of sqlPatterns) { if (pattern.test(sanitizedText)) { warnings.push('Potential SQL injection pattern detected in input'); break; } } return { sanitizedText, warnings }; } /** * Validate file paths to prevent directory traversal */ static validateFilePath(filePath) { if (!filePath || typeof filePath !== 'string') { return { isValid: false, error: 'File path must be a non-empty string' }; } const trimmedPath = filePath.trim(); // Check for directory traversal attempts const dangerousPatterns = [ '../', '..\\', '/etc/', '/var/', '/tmp/', '/usr/', '/bin/', '/sbin/', 'C:\\', 'D:\\', '\\\\' ]; for (const pattern of dangerousPatterns) { if (trimmedPath.includes(pattern)) { return { isValid: false, error: 'File path contains dangerous directory traversal patterns' }; } } // Only allow relative paths in contracts/ directory if (!trimmedPath.startsWith('contracts/') && !trimmedPath.match(/^[a-zA-Z0-9_\-\/\.]+$/)) { return { isValid: false, error: 'File path must be in contracts/ directory with valid characters only' }; } // Check for valid file extension const allowedExtensions = ['.py', '.gl', '.genlayer']; const hasValidExtension = allowedExtensions.some(ext => trimmedPath.endsWith(ext)); if (!hasValidExtension) { return { isValid: false, error: 'File must have a valid extension (.py, .gl, .genlayer)' }; } return { isValid: true, sanitizedPath: trimmedPath }; } /** * Comprehensive input validation for tool parameters */ static validateToolInput(toolName, params) { const errors = []; const warnings = []; const sanitizedParams = {}; if (!params || typeof params !== 'object') { return { isValid: false, errors: ['Parameters must be a valid object'], warnings: [] }; } // Validate URLs in parameters for (const [key, value] of Object.entries(params)) { if (key.includes('url') && typeof value === 'string') { const urlValidation = this.validateUrl(value); if (!urlValidation.isValid) { errors.push(`Invalid URL in ${key}: ${urlValidation.error}`); } else { sanitizedParams[key] = urlValidation.sanitizedUrl; } } else if (key.includes('code') && typeof value === 'string') { const codeValidation = this.sanitizeContractCode(value); sanitizedParams[key] = codeValidation.sanitizedCode; warnings.push(...codeValidation.warnings); } else if (key.includes('name') && typeof value === 'string') { const nameType = key.includes('contract') ? 'contract' : key.includes('method') ? 'method' : 'variable'; const nameValidation = this.validateIdentifier(value, nameType); if (!nameValidation.isValid) { errors.push(`Invalid ${nameType} name in ${key}: ${nameValidation.error}`); } else { sanitizedParams[key] = value.trim(); } } else if (typeof value === 'string') { const textValidation = this.sanitizeTextInput(value); sanitizedParams[key] = textValidation.sanitizedText; warnings.push(...textValidation.warnings); } else { sanitizedParams[key] = value; } } return { isValid: errors.length === 0, sanitizedParams: errors.length === 0 ? sanitizedParams : undefined, errors, warnings }; } } //# sourceMappingURL=security.js.map