Skip to main content
Glama
security-middleware.ts18.4 kB
/** * Security middleware for MCP tool execution */ import { logger } from '../utils/logger'; import { inputSanitizer } from './input-sanitizer'; import { rateLimiter } from './rate-limiter'; import { securityAuditor } from './security-audit'; import { resourceManager } from '../utils/resource-manager'; export interface SecurityContext { operation: string; clientId?: string; userAgent?: string; ipAddress?: string; parameters: Record<string, unknown>; } export interface SecurityCheckResult { allowed: boolean; sanitizedParameters?: Record<string, unknown>; errors: string[]; warnings: string[]; rateLimitInfo?: { remainingRequests: number; resetTime: number; retryAfter?: number; }; } export class SecurityMiddleware { private static instance: SecurityMiddleware; private constructor() {} public static getInstance(): SecurityMiddleware { if (!SecurityMiddleware.instance) { SecurityMiddleware.instance = new SecurityMiddleware(); } return SecurityMiddleware.instance; } /** * Comprehensive security check before tool execution */ public async checkSecurity( context: SecurityContext ): Promise<SecurityCheckResult> { const { operation, clientId = 'default', parameters } = context; const errors: string[] = []; const warnings: string[] = []; let sanitizedParameters = { ...parameters }; logger.debug(`Security check for operation: ${operation}`, { tool: operation, clientId, }); try { // 1. Rate limiting check const rateLimitResult = rateLimiter.checkRateLimit(operation, clientId); if (!rateLimitResult.allowed) { errors.push( `Rate limit exceeded. Try again in ${rateLimitResult.retryAfter} seconds.` ); securityAuditor.logSecurityEvent({ type: 'rate_limit', severity: 'medium', operation, clientId, details: { limit: 'exceeded', retryAfter: rateLimitResult.retryAfter, }, }); return { allowed: false, errors, warnings, rateLimitInfo: rateLimitResult, }; } // 2. Resource availability check const resourceAllowed = await resourceManager.shouldAllowRequest(operation); if (!resourceAllowed) { errors.push('Server is under high load. Please try again later.'); securityAuditor.logSecurityEvent({ type: 'resource_abuse', severity: 'high', operation, clientId: clientId || 'unknown', details: { reason: 'resource_exhaustion', resourceStatus: resourceManager.getResourceStatus(), }, }); return { allowed: false, errors, warnings, rateLimitInfo: rateLimitResult, }; } // 3. Operation-specific security checks const operationAllowed = resourceManager.isOperationAllowed(operation); if (!operationAllowed) { errors.push( 'Operation temporarily disabled due to resource constraints.' ); securityAuditor.logSecurityEvent({ type: 'access_denied', severity: 'medium', operation, clientId: clientId || 'unknown', details: { reason: 'operation_disabled', }, }); return { allowed: false, errors, warnings, rateLimitInfo: rateLimitResult, }; } // 4. Suspicious activity detection (before sanitization to catch malicious input) const suspiciousActivity = this.detectSuspiciousActivity( context, parameters // Use original parameters, not sanitized ); if (suspiciousActivity.isSuspicious) { if (suspiciousActivity.severity === 'critical') { errors.push('Request blocked due to suspicious activity.'); return { allowed: false, errors, warnings, rateLimitInfo: rateLimitResult, }; } else { warnings.push('Suspicious activity detected - request monitored.'); } } // 5. Input validation and sanitization const sanitizationResult = await this.sanitizeParameters( operation, parameters ); sanitizedParameters = sanitizationResult.sanitized; if (sanitizationResult.errors.length > 0) { errors.push(...sanitizationResult.errors); } if (sanitizationResult.warnings.length > 0) { warnings.push(...sanitizationResult.warnings); } // 6. Final validation const validationResult = await this.validateFinalParameters( operation, sanitizedParameters ); if (!validationResult.isValid) { errors.push(...validationResult.errors); } const allowed = errors.length === 0; if (allowed) { logger.debug(`Security check passed for ${operation}`, { tool: operation, clientId, warnings: warnings.length, }); } else { logger.warn(`Security check failed for ${operation}`, { tool: operation, clientId, errors: errors.length, warnings: warnings.length, }); } const result: SecurityCheckResult = { allowed, errors, warnings, rateLimitInfo: rateLimitResult, }; if (allowed) { result.sanitizedParameters = sanitizedParameters; } return result; } catch (error) { logger.error(`Security check error for ${operation}`, { tool: operation, error: error as Error, }); const logContext: Record<string, unknown> = { error: (error as Error).message, stack: (error as Error).stack, }; securityAuditor.logSecurityEvent({ type: 'suspicious_activity', severity: 'high', operation, clientId: clientId || 'unknown', details: logContext, }); return { allowed: false, errors: ['Security check failed due to internal error.'], warnings, }; } } /** * Sanitize parameters based on operation type */ private async sanitizeParameters( operation: string, parameters: Record<string, unknown> ): Promise<{ sanitized: Record<string, unknown>; errors: string[]; warnings: string[]; }> { const sanitized = { ...parameters }; const errors: string[] = []; const warnings: string[] = []; // Color input sanitization const colorFields = [ 'color', 'base_color', 'primary_color', 'foreground', 'background', ]; for (const field of colorFields) { if (sanitized[field] && typeof sanitized[field] === 'string') { const result = inputSanitizer.sanitizeColorInput( sanitized[field] as string ); sanitized[field] = result.sanitized; if (result.wasModified) { warnings.push(`Color input '${field}' was sanitized.`); securityAuditor.logSecurityEvent({ type: 'input_validation', severity: 'low', operation, details: { field, securityIssues: result.securityIssues, }, }); } } } // Array of colors sanitization const colorArrayFields = ['colors', 'palette', 'color_sets']; for (const field of colorArrayFields) { if (Array.isArray(sanitized[field])) { const colorArray = sanitized[field] as string[]; const sanitizedArray: string[] = []; for (let i = 0; i < colorArray.length; i++) { const colorValue = colorArray[i]; if (typeof colorValue === 'string') { const result = inputSanitizer.sanitizeColorInput(colorValue); sanitizedArray.push(result.sanitized); if (result.wasModified) { warnings.push(`Color in array '${field}[${i}]' was sanitized.`); } } else if (colorValue !== undefined) { sanitizedArray.push(colorValue); } } sanitized[field] = sanitizedArray; } } // URL sanitization const urlFields = ['image_url']; for (const field of urlFields) { if (sanitized[field] && typeof sanitized[field] === 'string') { const result = inputSanitizer.sanitizeUrl(sanitized[field] as string); sanitized[field] = result.sanitized; if (result.wasModified) { if (result.securityIssues.some(issue => issue.includes('Blocked'))) { errors.push( `URL '${field}' contains security risks and was blocked.` ); } else { warnings.push(`URL '${field}' was sanitized.`); } securityAuditor.logSecurityEvent({ type: 'input_validation', severity: result.securityIssues.some(issue => issue.includes('Blocked') ) ? 'high' : 'medium', operation, details: { field, securityIssues: result.securityIssues, }, }); } } } // HTML content sanitization const htmlFields = ['html_content', 'template']; for (const field of htmlFields) { if (sanitized[field] && typeof sanitized[field] === 'string') { const result = inputSanitizer.sanitizeHtml(sanitized[field] as string, { allowHtml: true, maxLength: 100000, }); sanitized[field] = result.sanitized; if (result.wasModified) { warnings.push(`HTML content '${field}' was sanitized.`); securityAuditor.logSecurityEvent({ type: 'input_validation', severity: result.securityIssues.length > 0 ? 'medium' : 'low', operation, details: { field, securityIssues: result.securityIssues, removedElements: result.removedElements.length, }, }); } } } // Numeric parameter validation const numericFields = [ 'precision', 'count', 'size', 'resolution', 'angle', 'duration', ]; for (const field of numericFields) { if (sanitized[field] !== undefined) { const value = sanitized[field]; if (typeof value === 'number') { // Check for reasonable bounds if (!Number.isFinite(value) || value < 0 || value > 10000) { errors.push( `Numeric parameter '${field}' is out of acceptable range.` ); } } else if (typeof value === 'string') { const parsed = parseFloat(value); if (!Number.isFinite(parsed) || parsed < 0 || parsed > 10000) { errors.push(`Numeric parameter '${field}' is not a valid number.`); } else { sanitized[field] = parsed; } } } } return { sanitized, errors, warnings }; } /** * Detect suspicious activity patterns */ private detectSuspiciousActivity( context: SecurityContext, parameters: Record<string, unknown> ): { isSuspicious: boolean; severity: 'low' | 'medium' | 'high' | 'critical'; reasons: string[]; } { const { operation, clientId = 'default' } = context; const reasons: string[] = []; let maxSeverity: 'low' | 'medium' | 'high' | 'critical' = 'low'; // Check for rapid successive requests const recentRequests = securityAuditor.getClientEventCount(clientId, 60000); // Last minute if (recentRequests > 100) { reasons.push('Excessive request rate'); maxSeverity = 'high'; } else if (recentRequests > 50) { reasons.push('High request rate'); maxSeverity = 'medium'; } // Check for suspicious parameter patterns const paramString = JSON.stringify(parameters); const analysis = securityAuditor.analyzeInput( paramString, operation, clientId ); if (analysis.isSuspicious) { reasons.push(...analysis.suspiciousPatterns); if (analysis.riskScore > 30) { maxSeverity = 'critical'; } else if (analysis.riskScore > 15) { maxSeverity = maxSeverity === 'high' ? 'high' : 'medium'; } else { maxSeverity = maxSeverity === 'high' || maxSeverity === 'medium' ? maxSeverity : 'low'; } } // Check for unusual operation patterns const expensiveOps = [ 'create_palette_png', 'create_gradient_png', 'extract_palette_from_image', ]; if (expensiveOps.includes(operation)) { const expensiveRequests = securityAuditor.getClientEventCount( clientId, 300000 ); // Last 5 minutes if (expensiveRequests > 10) { reasons.push('Excessive expensive operations'); maxSeverity = maxSeverity === 'critical' ? 'critical' : 'high'; } } // Check for parameter size abuse const paramSize = JSON.stringify(parameters).length; if (paramSize > 100000) { // 100KB reasons.push('Excessive parameter size'); maxSeverity = maxSeverity === 'critical' ? 'critical' : 'medium'; } // Check client risk score and event history const clientRiskScore = securityAuditor.getClientRiskScore(clientId); const totalClientEvents = securityAuditor.getClientEventCount( clientId, 24 * 60 * 60 * 1000 ); // Last 24 hours if (clientRiskScore >= 80 || totalClientEvents > 100) { reasons.push('High-risk client with extensive security event history'); maxSeverity = 'critical'; } else if (clientRiskScore >= 50 || totalClientEvents > 50) { reasons.push('Medium-risk client with security concerns'); maxSeverity = maxSeverity === 'critical' ? 'critical' : 'high'; } const isSuspicious = reasons.length > 0; if (isSuspicious) { securityAuditor.logSecurityEvent({ type: 'suspicious_activity', severity: maxSeverity, operation, clientId, details: { reasons, recentRequests, parameterSize: paramSize, riskScore: analysis.riskScore, }, }); } return { isSuspicious, severity: maxSeverity, reasons, }; } /** * Final parameter validation */ private async validateFinalParameters( operation: string, parameters: Record<string, unknown> ): Promise<{ isValid: boolean; errors: string[]; }> { const errors: string[] = []; // Operation-specific validation switch (operation) { case 'extract_palette_from_image': if ( !parameters['image_url'] || typeof parameters['image_url'] !== 'string' ) { errors.push('image_url is required and must be a string'); } else if (parameters['image_url'] === 'about:blank') { errors.push('Invalid or blocked image URL'); } break; case 'create_palette_png': case 'create_gradient_png': // Check for reasonable image dimensions if ( parameters['dimensions'] && Array.isArray(parameters['dimensions']) ) { const dimensions = parameters['dimensions'] as number[]; const [width, height] = dimensions; if (width && height) { if (width > 10000 || height > 10000) { errors.push('Image dimensions too large (max 10000x10000)'); } if (width * height > 50000000) { // 50 megapixels errors.push('Image area too large (max 50 megapixels)'); } } } break; case 'generate_harmony_palette': case 'generate_contextual_palette': // Limit palette size if ( parameters['count'] && typeof parameters['count'] === 'number' && parameters['count'] > 50 ) { errors.push('Palette size too large (max 50 colors)'); } break; } // Check for required parameters based on operation const requiredParams = this.getRequiredParameters(operation); for (const param of requiredParams) { if (parameters[param] === undefined || parameters[param] === null) { errors.push(`Required parameter '${param}' is missing`); } } return { isValid: errors.length === 0, errors, }; } /** * Get required parameters for each operation */ private getRequiredParameters(operation: string): string[] { const requiredParams: Record<string, string[]> = { convert_color: ['color', 'output_format'], analyze_color: ['color'], generate_harmony_palette: ['base_color', 'harmony_type'], generate_contextual_palette: ['context'], generate_algorithmic_palette: ['algorithm'], extract_palette_from_image: ['image_url'], create_palette_html: ['palette'], create_palette_png: ['palette'], create_color_wheel_html: [], create_gradient_html: ['gradient_css'], create_gradient_png: ['gradient', 'dimensions'], check_contrast: ['foreground', 'background'], simulate_colorblindness: ['colors', 'type'], optimize_for_accessibility: ['palette', 'use_cases'], mix_colors: ['colors'], generate_color_variations: ['base_color', 'variation_type'], sort_colors: ['colors', 'sort_by'], analyze_color_collection: ['colors'], }; return requiredParams[operation] || []; } /** * Get security headers for HTTP responses */ public getSecurityHeaders(): Record<string, string> { return { 'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self';", 'X-Content-Type-Options': 'nosniff', 'X-Frame-Options': 'DENY', 'X-XSS-Protection': '1; mode=block', 'Referrer-Policy': 'strict-origin-when-cross-origin', 'Permissions-Policy': 'camera=(), microphone=(), geolocation=()', }; } } // Export singleton instance export const securityMiddleware = SecurityMiddleware.getInstance();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/keyurgolani/ColorMcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server