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
JavaScript
/**
* 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