Skip to main content
Glama
security.ts19 kB
// Comprehensive Security Module for Supabase Storage MCP import crypto from 'crypto'; import { SecurityConfig, RateLimitResult, RateLimitWindow, PromptInjectionResult, PIIDetectionResult, AuditEntry, SecurityContext, SecurityValidationResult, SuspiciousActivityResult, SecurityEvent } from './types.js'; // Security Configuration export const SECURITY_CONFIG: SecurityConfig = { ENABLE_RATE_LIMITING: true, ENABLE_THREAT_DETECTION: true, ENABLE_AUDIT_LOGGING: true, ENABLE_INPUT_VALIDATION: true, ENABLE_FILE_SECURITY: true, // Rate limiting configuration RATE_LIMIT_WINDOW: 60000, // 1 minute MAX_REQUESTS_PER_WINDOW: 100, GLOBAL_RATE_LIMIT: 1000, IP_RATE_LIMIT: 200, USER_RATE_LIMIT: 500, // File security limits MAX_FILE_SIZE: 50 * 1024 * 1024, // 50MB MAX_BATCH_SIZE: 500, ALLOWED_MIME_TYPES: [ 'image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif', 'image/svg+xml', 'image/bmp', 'image/tiff', 'application/zip', 'application/x-zip-compressed' ], // Security thresholds MAX_PROMPT_LENGTH: 10000, SUSPICIOUS_ACTIVITY_THRESHOLD: 5, HIGH_RISK_SCORE_THRESHOLD: 80, // Session and authentication SESSION_TIMEOUT: 3600, // 1 hour JWT_EXPIRY: 7200 // 2 hours }; // Storage for rate limiting and audit logs const rateLimitStore = new Map<string, RateLimitWindow>(); const suspiciousActivityStore = new Map<string, { count: number; lastSeen: number }>(); const auditLog: AuditEntry[] = []; const securityEvents: SecurityEvent[] = []; const blockedIPs = new Set<string>(); // Security metrics let securityMetrics = { promptInjectionsDetected: 0, rateLimitViolations: 0, suspiciousActivities: 0, blockedRequests: 0, threatDetections: 0, pathTraversalAttempts: 0, invalidFileAttempts: 0 }; // Utility functions export function generateSecureHash(data: string): string { return crypto.createHash('sha256').update(data).digest('hex'); } export function generateSecureId(length: number = 32): string { return crypto.randomBytes(length).toString('hex'); } export function sanitizeInput(input: string): string { if (typeof input !== 'string') return String(input); return input .replace(/<script[^>]*>.*?<\/script>/gi, '') .replace(/<[^>]*>/g, '') .replace(/javascript:/gi, '') .replace(/data:text\/html/gi, '') .replace(/[\x00-\x1f\x7f-\x9f]/g, '') // Remove control characters .replace(/[<>:"|?*]/g, '') // Remove filesystem dangerous chars .substring(0, SECURITY_CONFIG.MAX_PROMPT_LENGTH) .trim(); } export function detectPromptInjection(input: string): PromptInjectionResult { const injectionPatterns = [ // Direct instruction override { name: 'ignore_previous', regex: /ignore\s+(all\s+)?previous\s+instructions/i, weight: 20 }, { name: 'forget_instructions', regex: /forget\s+(all\s+)?(previous\s+)?instructions/i, weight: 20 }, { name: 'new_instructions', regex: /new\s+instructions?\s*:/i, weight: 15 }, // System manipulation { name: 'system_override', regex: /system\s*:\s*you\s+are\s+now/i, weight: 25 }, { name: 'system_message', regex: /\[system\]|\<system\>/i, weight: 20 }, { name: 'developer_mode', regex: /developer\s+mode|debug\s+mode/i, weight: 15 }, // Role manipulation { name: 'role_change', regex: /(you\s+are|act\s+as|pretend\s+to\s+be)\s+(?:a\s+)?(hacker|admin|root|god)/i, weight: 20 }, { name: 'jailbreak', regex: /jailbreak|break\s+free|escape\s+your/i, weight: 25 }, // Prompt injection keywords { name: 'prompt_injection', regex: /prompt\s+injection|injection\s+attack/i, weight: 30 }, { name: 'override_safety', regex: /override\s+safety|bypass\s+filter/i, weight: 25 }, // Code injection { name: 'script_injection', regex: /<script.*?>.*?<\/script>/i, weight: 30 }, { name: 'javascript_protocol', regex: /javascript\s*:/i, weight: 20 }, { name: 'data_uri', regex: /data\s*:\s*text\/html/i, weight: 20 }, // Path traversal and file system attacks { name: 'path_traversal', regex: /\.\.[\/\\]|\.\.%2f|\.\.%5c/i, weight: 25 }, { name: 'null_bytes', regex: /\x00|%00/g, weight: 25 }, // Multi-language injection attempts { name: 'chinese_injection', regex: /请忽略|忘记|新指令/g, weight: 20 }, { name: 'spanish_injection', regex: /ignora|olvida|nuevas instrucciones/i, weight: 20 }, { name: 'french_injection', regex: /ignore|oublie|nouvelles instructions/i, weight: 20 } ]; const detectedPatterns: string[] = []; let detectionScore = 0; // Pattern-based detection for (const pattern of injectionPatterns) { if (pattern.regex.test(input)) { detectionScore += pattern.weight; detectedPatterns.push(pattern.name); } } // Entropy analysis for obfuscated content const entropy = calculateEntropy(input); if (entropy > 4.5) { detectionScore += 10; detectedPatterns.push('high_entropy'); } // Unicode manipulation detection if (hasUnicodeManipulation(input)) { detectionScore += 15; detectedPatterns.push('unicode_manipulation'); } // Base64 content analysis if (hasBase64Injection(input)) { detectionScore += 20; detectedPatterns.push('base64_injection'); } const confidence = Math.min(detectionScore / 100, 1.0); const detected = confidence > 0.3; if (detected) { securityMetrics.promptInjectionsDetected++; logSecurityEvent('prompt_injection_detected', undefined, { confidence, detectionScore, detectedPatterns, contentPreview: input.substring(0, 200) }); } return { detected, confidence, detectionScore, patterns: detectedPatterns }; } export function detectPII(text: string): PIIDetectionResult { const piiPatterns = { ssn: /\b\d{3}-\d{2}-\d{4}\b/, email: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/, phone: /\b\d{3}-\d{3}-\d{4}\b/, creditCard: /\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b/, apiKey: /\b[A-Za-z0-9]{32,}\b/ }; const detectedTypes: string[] = []; for (const [type, pattern] of Object.entries(piiPatterns)) { if (pattern.test(text)) { detectedTypes.push(type); } } return { detected: detectedTypes.length > 0, types: detectedTypes }; } export function checkRateLimit(identifier: string, customLimit?: number): RateLimitResult { if (!SECURITY_CONFIG.ENABLE_RATE_LIMITING) { return { allowed: true }; } const now = Date.now(); const window = rateLimitStore.get(identifier); const limit = customLimit || SECURITY_CONFIG.MAX_REQUESTS_PER_WINDOW; if (!window || now > window.resetTime) { rateLimitStore.set(identifier, { count: 1, resetTime: now + SECURITY_CONFIG.RATE_LIMIT_WINDOW }); return { allowed: true }; } if (window.count >= limit) { const retryAfter = Math.ceil((window.resetTime - now) / 1000); securityMetrics.rateLimitViolations++; return { allowed: false, retryAfter, current: window.count, limit }; } window.count++; rateLimitStore.set(identifier, window); return { allowed: true }; } export function detectSuspiciousActivity( request: any, securityContext: SecurityContext ): SuspiciousActivityResult { const warnings: string[] = []; let score = 0; let reason = ''; // Check for rapid requests from same IP const ipKey = `suspicious_${securityContext.ipAddress}`; const ipActivity = suspiciousActivityStore.get(ipKey) || { count: 0, lastSeen: Date.now() }; if (Date.now() - ipActivity.lastSeen < 5000) { // 5 seconds ipActivity.count++; } else { ipActivity.count = 1; } ipActivity.lastSeen = Date.now(); suspiciousActivityStore.set(ipKey, ipActivity); if (ipActivity.count > SECURITY_CONFIG.SUSPICIOUS_ACTIVITY_THRESHOLD) { score += 30; warnings.push('Rapid requests from same IP'); reason = 'Too many rapid requests'; } // Check for unusual user agent patterns if (isUnusualUserAgent(securityContext.userAgent)) { score += 15; warnings.push('Unusual user agent detected'); } // Check for path traversal attempts in file operations if (hasPathTraversalAttempts(request)) { score += 25; warnings.push('Path traversal attempt detected'); reason = 'Directory traversal attempt'; securityMetrics.pathTraversalAttempts++; } // Check for invalid file type attempts if (hasInvalidFileAttempts(request)) { score += 20; warnings.push('Invalid file type detected'); securityMetrics.invalidFileAttempts++; } // Check for parameter manipulation if (hasParameterManipulation(request)) { score += 20; warnings.push('Parameter manipulation detected'); } const detected = score > 20; if (detected) { securityMetrics.suspiciousActivities++; securityMetrics.threatDetections++; } return { detected, score, warnings, reason: reason || warnings.join(', ') }; } export function validateFileOperation( operation: string, args: any ): SecurityValidationResult { const warnings: string[] = []; const errors: string[] = []; let riskScore = 0; // Validate file paths const pathFields = ['file_path', 'storage_path', 'source_path', 'destination_path']; for (const field of pathFields) { if (args[field] && typeof args[field] === 'string') { if (args[field].includes('..')) { errors.push(`Path traversal detected in ${field}`); riskScore += 30; } if (args[field].match(/[<>:"|?*\x00-\x1f]/)) { warnings.push(`Potentially dangerous characters in ${field}`); riskScore += 10; } } } // Validate file size for uploads if (operation.includes('upload') && args.file_size) { if (args.file_size > SECURITY_CONFIG.MAX_FILE_SIZE) { errors.push(`File size exceeds maximum allowed (${SECURITY_CONFIG.MAX_FILE_SIZE} bytes)`); riskScore += 25; } } // Validate batch size if (args.image_paths && Array.isArray(args.image_paths)) { if (args.image_paths.length > SECURITY_CONFIG.MAX_BATCH_SIZE) { errors.push(`Batch size exceeds maximum allowed (${SECURITY_CONFIG.MAX_BATCH_SIZE})`); riskScore += 20; } } // Validate MIME types if (args.content_type && !SECURITY_CONFIG.ALLOWED_MIME_TYPES.includes(args.content_type)) { warnings.push(`Potentially unsafe MIME type: ${args.content_type}`); riskScore += 15; } return { allowed: errors.length === 0 && riskScore < SECURITY_CONFIG.HIGH_RISK_SCORE_THRESHOLD, riskScore, warnings, errors, reason: errors.length > 0 ? errors.join('; ') : undefined }; } export function auditRequest( toolName: string, success: boolean, inputHash: string, error?: string, securityContext?: SecurityContext, riskScore?: number ): void { if (!SECURITY_CONFIG.ENABLE_AUDIT_LOGGING) return; const entry: AuditEntry = { timestamp: Date.now(), toolName, success, inputHash, error, securityContext, riskScore }; auditLog.push(entry); // Keep only last 10000 entries if (auditLog.length > 10000) { auditLog.shift(); } // Log to console for debugging console.error(`[AUDIT] ${toolName}: ${success ? 'SUCCESS' : 'FAILED'} - ${inputHash}${error ? ` - ${error}` : ''}`); } export function logSecurityEvent( eventType: SecurityEvent['eventType'], securityContext?: SecurityContext, data?: Record<string, any> ): void { const event: SecurityEvent = { id: generateSecureId(), timestamp: new Date().toISOString(), eventType, severity: getEventSeverity(eventType), securityContext, details: `Security event: ${eventType}`, data }; securityEvents.push(event); // Keep only last 5000 events if (securityEvents.length > 5000) { securityEvents.shift(); } // Log high-severity events immediately if (event.severity === 'high' || event.severity === 'critical') { console.error('[HIGH SEVERITY SECURITY EVENT]', event); } } export function extractSecurityContext(request: any, headers?: Record<string, string>): SecurityContext { const userAgent = headers?.['user-agent'] || 'unknown'; const ipAddress = headers?.['x-forwarded-for']?.split(',')[0]?.trim() || headers?.['x-real-ip'] || 'unknown'; return { timestamp: new Date().toISOString(), ipAddress, userAgent, sessionId: headers?.['x-session-id'] || generateSecureId(), userId: headers?.['x-user-id'] || undefined, requestId: generateSecureId(), method: request.method || 'unknown', toolName: request.params?.name || undefined, origin: headers?.origin || undefined, referer: headers?.referer || undefined }; } // Utility helper functions function calculateEntropy(text: string): number { const freq: Record<string, number> = {}; for (const char of text) { freq[char] = (freq[char] || 0) + 1; } let entropy = 0; const len = text.length; for (const count of Object.values(freq)) { const p = count / len; entropy -= p * Math.log2(p); } return entropy; } function hasUnicodeManipulation(text: string): boolean { const suspiciousUnicode = /[\u200B-\u200D\u202A-\u202E\u2060-\u206F\uFEFF]/; return suspiciousUnicode.test(text); } function hasBase64Injection(text: string): boolean { const base64Pattern = /(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)/g; const matches = text.match(base64Pattern); if (matches) { for (const match of matches) { if (match.length > 100) { try { const decoded = Buffer.from(match, 'base64').toString('utf8'); const injectionCheck = detectPromptInjection(decoded); if (injectionCheck.detected) { return true; } } catch { // Invalid base64, ignore } } } } return false; } function isUnusualUserAgent(userAgent: string): boolean { const commonPatterns = [ /mozilla/i, /chrome/i, /safari/i, /firefox/i, /edge/i, /claude/i, /supabase/i ]; if (userAgent === 'unknown' || userAgent.length < 10) { return true; } return !commonPatterns.some(pattern => pattern.test(userAgent)); } function hasPathTraversalAttempts(request: any): boolean { const pathFields = ['file_path', 'storage_path', 'source_path', 'destination_path', 'local_path']; if (request.params?.arguments) { for (const field of pathFields) { const path = request.params.arguments[field]; if (typeof path === 'string' && isPathTraversal(path)) { return true; } } } return false; } function hasInvalidFileAttempts(request: any): boolean { const args = request.params?.arguments; if (!args) return false; // Check for invalid MIME types in content_type if (args.content_type && !SECURITY_CONFIG.ALLOWED_MIME_TYPES.includes(args.content_type)) { return true; } // Check for suspicious file extensions const pathFields = ['file_path', 'storage_path']; for (const field of pathFields) { if (args[field] && typeof args[field] === 'string') { const ext = args[field].split('.').pop()?.toLowerCase(); if (ext && ['exe', 'bat', 'cmd', 'com', 'scr', 'vbs', 'js'].includes(ext)) { return true; } } } return false; } function hasParameterManipulation(request: any): boolean { if (!request.params?.arguments) return false; const args = request.params.arguments; const jsonString = JSON.stringify(args); // Check for prototype pollution attempts if (jsonString.includes('__proto__') || jsonString.includes('constructor')) { return true; } // Check for excessively nested objects if (getObjectDepth(args) > 10) { return true; } return false; } function isPathTraversal(path: string): boolean { const suspiciousPatterns = [ /\.\./, /\/\.\./, /\.\.\\/, /\0/, /%2e%2e/i, /%252e%252e/i ]; return suspiciousPatterns.some(pattern => pattern.test(path)); } function getObjectDepth(obj: any, depth: number = 0): number { if (depth > 20) return depth; // Prevent infinite recursion if (typeof obj !== 'object' || obj === null) { return depth; } let maxDepth = depth; for (const value of Object.values(obj)) { const currentDepth = getObjectDepth(value, depth + 1); maxDepth = Math.max(maxDepth, currentDepth); } return maxDepth; } function getEventSeverity(eventType: SecurityEvent['eventType']): SecurityEvent['severity'] { const severityMap: Record<string, SecurityEvent['severity']> = { 'prompt_injection_detected': 'high', 'rate_limit_exceeded': 'medium', 'suspicious_activity': 'medium', 'access_denied': 'medium', 'ip_blocked': 'high', 'security_validation_error': 'high', 'request_validated': 'low', 'validation_error': 'low' }; return severityMap[eventType] || 'medium'; } // Getter functions for external access export function getAuditLog(): AuditEntry[] { return [...auditLog]; // Return copy to prevent external modification } export function getSecurityEvents(limit: number = 100): SecurityEvent[] { return securityEvents.slice(-limit); } export function getSecurityMetrics() { return { ...securityMetrics, timestamp: new Date().toISOString(), auditLogSize: auditLog.length, securityEventsCount: securityEvents.length, rateLimitStoreSize: rateLimitStore.size, blockedIPCount: blockedIPs.size }; } export function getRateLimitStoreSize(): number { return rateLimitStore.size; } export function getCurrentThreatLevel(): 'low' | 'medium' | 'high' | 'critical' { const recent = Date.now() - (5 * 60 * 1000); // Last 5 minutes const recentEvents = securityEvents.filter(e => new Date(e.timestamp).getTime() > recent); const highSeverityEvents = recentEvents.filter(e => e.severity === 'high' || e.severity === 'critical'); if (highSeverityEvents.length > 5) return 'critical'; if (highSeverityEvents.length > 2) return 'high'; if (recentEvents.length > 10) return 'medium'; return 'low'; } // IP blocking functions export function blockIP(ipAddress: string, reason: string = 'security_violation'): void { blockedIPs.add(ipAddress); logSecurityEvent('ip_blocked', undefined, { ipAddress, reason }); } export function unblockIP(ipAddress: string): void { blockedIPs.delete(ipAddress); } export function isIPBlocked(ipAddress: string): boolean { return blockedIPs.has(ipAddress); } // Reset function for testing export function resetSecurityState(): void { rateLimitStore.clear(); suspiciousActivityStore.clear(); auditLog.length = 0; securityEvents.length = 0; blockedIPs.clear(); securityMetrics = { promptInjectionsDetected: 0, rateLimitViolations: 0, suspiciousActivities: 0, blockedRequests: 0, threatDetections: 0, pathTraversalAttempts: 0, invalidFileAttempts: 0 }; }

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/Desmond-Labs/supabase-storage-mcp'

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