UNPKG

mcp-cve-intelligence-server-lite-test

Version:

Lite Model Context Protocol server for comprehensive CVE intelligence gathering with multi-source exploit discovery, designed for security professionals and cybersecurity researchers - Alpha Release

276 lines 12.3 kB
/** * Input sanitization and validation middleware * Provides request sanitization to prevent security vulnerabilities */ import { createContextLogger } from '../utils/logger.js'; import { secureErrorHandler, ErrorType } from '../utils/secure-error-handler.js'; const logger = createContextLogger('InputSanitization'); // Maximum sizes for different input types const MAX_URL_LENGTH = 2048; const DEFAULT_OPTIONS = { maxUrlLength: MAX_URL_LENGTH, allowedMethods: ['GET', 'POST', 'DELETE'], // MCP spec: GET (SSE streams), POST (JSON-RPC), DELETE (session termination) enableRateLimiting: true, rateLimitConfig: { windowMs: 15 * 60 * 1000, // 15 minutes maxRequests: 200, // Reasonable limit for CVE research workflows skipSuccessfulRequests: false, }, }; // Simple in-memory rate limiter class SimpleRateLimiter { config; requests = new Map(); constructor(config) { this.config = config; } isAllowed(clientId) { const now = Date.now(); const clientData = this.requests.get(clientId); if (!clientData || now > clientData.resetTime) { // New client or window expired this.requests.set(clientId, { count: 1, resetTime: now + this.config.windowMs, }); return true; } if (clientData.count >= this.config.maxRequests) { return false; } clientData.count++; return true; } cleanup() { const now = Date.now(); for (const [clientId, data] of this.requests.entries()) { if (now > data.resetTime) { this.requests.delete(clientId); } } } } export class InputSanitizationMiddleware { options; rateLimiter; constructor(options = {}) { this.options = { ...DEFAULT_OPTIONS, ...options }; if (this.options.enableRateLimiting) { this.rateLimiter = new SimpleRateLimiter(this.options.rateLimitConfig); // Cleanup expired entries every 5 minutes setInterval(() => this.rateLimiter?.cleanup(), 5 * 60 * 1000); } } /** * Express middleware function */ middleware = (req, res, next) => { try { // Only allow specific endpoints: /health and /mcp const isHealthEndpoint = req.path === '/health'; const isMCPEndpoint = req.path === '/mcp'; // Block all other endpoints if (!isHealthEndpoint && !isMCPEndpoint) { logger.debug('Endpoint not allowed', { path: this.sanitizeForLog(req.path), method: req.method, allowedEndpoints: ['/health', '/mcp'], }); res.status(404).json({ error: 'Not Found', message: 'Endpoint not available', }); return; } // Rate limiting check (skip for health endpoints) if (this.rateLimiter && !isHealthEndpoint) { const clientId = this.getClientIdentifier(req); if (!this.rateLimiter.isAllowed(clientId)) { logger.warn('Rate limit exceeded', { clientId: this.sanitizeForLog(clientId), method: req.method, path: this.sanitizeForLog(req.path), userAgent: this.sanitizeForLog(req.get('User-Agent') || 'unknown'), }); res.status(429).json({ error: 'Too Many Requests', message: 'Rate limit exceeded. Please try again later.', retryAfter: Math.ceil(this.options.rateLimitConfig.windowMs / 1000), }); return; } } // Validate HTTP method if (!this.options.allowedMethods.includes(req.method)) { logger.warn('Invalid HTTP method', { method: req.method, path: this.sanitizeForLog(req.path), }); res.status(405).json({ error: 'Method Not Allowed', message: 'HTTP method not supported', }); return; } // Block query parameters completely - MCP uses JSON-RPC only if (req.url.includes('?')) { logger.warn('Query parameters not supported in MCP server', { url: this.sanitizeForLog(req.url), path: this.sanitizeForLog(req.path), }); const errorDetails = secureErrorHandler.createSafeError({ type: ErrorType.VALIDATION, originalError: new Error('Query parameters not supported'), statusCode: 400, userMessage: 'MCP servers use JSON-RPC protocol only. Query parameters are not supported.', }); res.status(400).json(errorDetails); return; } // MCP-specific Accept header validation (only for MCP endpoints) if (isMCPEndpoint) { const acceptHeader = req.get('Accept'); if (req.method === 'POST') { // MCP POST requests MUST include Accept: application/json, text/event-stream if (!acceptHeader || (!acceptHeader.includes('application/json') || !acceptHeader.includes('text/event-stream'))) { logger.warn('Invalid Accept header for MCP POST request', { acceptHeader }); const errorDetails = secureErrorHandler.createSafeError({ type: ErrorType.VALIDATION, originalError: new Error('Invalid Accept header'), statusCode: 400, userMessage: 'MCP POST requests must include Accept: application/json, text/event-stream', }); res.status(400).json(errorDetails); return; } } else if (req.method === 'GET') { // MCP GET requests MUST include Accept: text/event-stream if (!acceptHeader || !acceptHeader.includes('text/event-stream')) { logger.warn('Invalid Accept header for MCP GET request', { acceptHeader }); const errorDetails = secureErrorHandler.createSafeError({ type: ErrorType.VALIDATION, originalError: new Error('Invalid Accept header'), statusCode: 400, userMessage: 'MCP GET requests must include Accept: text/event-stream', }); res.status(400).json(errorDetails); return; } } } // MCP Session Management validation (only for MCP endpoints) if (isMCPEndpoint) { const sessionId = req.get('Mcp-Session-Id'); if (sessionId) { // Basic session ID validation - remove null bytes for security // No specific format validation needed - MCP transports handle session validation const sanitizedSessionId = this.sanitizeString(sessionId); if (sanitizedSessionId !== sessionId) { logger.warn('Session ID contained invalid characters', { originalLength: sessionId.length, sanitizedLength: sanitizedSessionId.length, }); const errorDetails = secureErrorHandler.createSafeError({ type: ErrorType.VALIDATION, originalError: new Error('Session ID contains invalid characters'), statusCode: 400, userMessage: 'Invalid characters in session ID', }); res.status(400).json(errorDetails); return; } } } // Sanitize request body (if present) if (req.body) { req.body = this.sanitizeBody(req.body); } // Apply security headers this.applySecurityHeaders(res); next(); } catch (error) { logger.error('Input sanitization failed', error); res.status(400).json({ error: 'Bad Request', message: 'Request validation failed', }); } }; getClientIdentifier(req) { // Use X-Forwarded-For if behind proxy, otherwise use remote address const forwarded = req.get('X-Forwarded-For'); const ip = forwarded ? forwarded.split(',')[0].trim() : req.ip || req.socket.remoteAddress || 'unknown'; return ip; } sanitizeBody(body) { if (typeof body === 'string') { return this.sanitizeString(body); } if (Array.isArray(body)) { return body.map(item => this.sanitizeBody(item)); } if (body && typeof body === 'object') { const sanitized = {}; for (const [key, value] of Object.entries(body)) { sanitized[this.sanitizeString(key)] = this.sanitizeBody(value); } return sanitized; } return body; } sanitizeString(input) { let sanitized = input; // Only remove null bytes (security issue) and dangerous control characters // Preserve all CVE content exactly as-is const nullByteRegex = /\0/g; const controlCharRegex = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g; // eslint-disable-line no-control-regex sanitized = sanitized .replace(nullByteRegex, '') // Remove null bytes .replace(controlCharRegex, ''); // Remove control chars but keep \t, \n, \r return sanitized; } applySecurityHeaders(res) { // Security headers appropriate for MCP servers with browsable health endpoints res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('X-Frame-Options', 'DENY'); res.setHeader('X-XSS-Protection', '1; mode=block'); res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); // Strict CSP for MCP server - no UI, only JSON API and health endpoints // Allow inline styles for basic health endpoint formatting if needed res.setHeader('Content-Security-Policy', "default-src 'none'; " + "script-src 'none'; " + "style-src 'unsafe-inline'; " + "img-src 'none'; " + "font-src 'none'; " + "connect-src 'none'; " + "media-src 'none'; " + "object-src 'none'; " + "child-src 'none'; " + "worker-src 'none'; " + "frame-src 'none'; " + "base-uri 'none'; " + "form-action 'none'"); // Only apply HSTS in production with HTTPS if (process.env.NODE_ENV === 'production') { res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); } // Remove potentially revealing headers res.removeHeader('X-Powered-By'); res.removeHeader('Server'); // Add MCP-specific headers res.setHeader('X-MCP-Server', 'CVE-Intelligence-Server'); res.setHeader('X-Service-Type', 'MCP-API-Server'); } sanitizeForLog(value) { // Sanitize values for safe logging return value .replace(/[^\w\s\-._~:/?#[\]@!$&'()*+,;=]/g, '') // Keep only safe characters .substring(0, 200); // Limit length } } // Export default instance export const inputSanitization = new InputSanitizationMiddleware(); //# sourceMappingURL=input-sanitization-middleware.js.map