UNPKG

mcp-quiz-server

Version:

🧠 AI-Powered Quiz Management via Model Context Protocol (MCP) - Create, manage, and take quizzes directly from VS Code, Claude, and other AI agents.

299 lines (298 loc) • 11 kB
"use strict"; /** * @moduleName: MCP Security Utilities * @version: 1.0.0 * @since: 2025-07-25 * @lastUpdated: 2025-07-25 * @projectSummary: Security utilities for MCP handler with input validation, rate limiting, and access control * @techStack: TypeScript, crypto, jwt * @dependency: crypto, jsonwebtoken (optional) * @interModuleDependency: Used by all MCP handlers * @requirementsTraceability: * {@link Requirements.REQ_MCP_003} (Resource Access Control) * {@link Requirements.REQ_SEC_001} (OWASP Input Sanitization) * @briefDescription: Security-focused utilities for validating inputs, managing tokens, and preventing attacks * @methods: validateInput, sanitizeParams, checkRateLimit, generateSecureId * @contributors: GitHub Copilot * @examples: * - SecurityUtils.validateInput(userInput, 'string', 100); * - SecurityUtils.checkRateLimit(clientId, 'tool_call'); * @vulnerabilitiesAssessment: Comprehensive input validation, rate limiting, XSS prevention, injection protection */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.SecurityUtils = void 0; const crypto = __importStar(require("crypto")); /** * Security utilities for MCP protocol operations */ class SecurityUtils { /** * Validate and sanitize input parameters */ static validateInput(input, expectedType, maxLength) { try { // Null/undefined check if (input === null || input === undefined) { return { isValid: false, error: 'Input cannot be null or undefined' }; } // Type validation const actualType = Array.isArray(input) ? 'array' : typeof input; if (actualType !== expectedType) { return { isValid: false, error: `Expected ${expectedType}, got ${actualType}` }; } // Length validation for strings if (expectedType === 'string') { const str = input; const effectiveMaxLength = maxLength || this.securityConfig.maxInputLength; if (str.length > effectiveMaxLength) { return { isValid: false, error: `String length exceeds maximum of ${effectiveMaxLength}`, }; } // Sanitize string input const sanitized = this.securityConfig.enableInputSanitization ? this.sanitizeString(str) : str; return { isValid: true, sanitized }; } // Object validation and sanitization if (expectedType === 'object') { const sanitized = this.sanitizeObject(input); return { isValid: true, sanitized }; } // Array validation if (expectedType === 'array') { const arr = input; if (arr.length > 1000) { // Prevent array DoS return { isValid: false, error: 'Array length exceeds maximum of 1000' }; } const sanitized = arr.map(item => typeof item === 'string' ? this.sanitizeString(item) : item); return { isValid: true, sanitized }; } // Number validation if (expectedType === 'number') { if (!Number.isFinite(input)) { return { isValid: false, error: 'Number must be finite' }; } } return { isValid: true, sanitized: input }; } catch (error) { return { isValid: false, error: `Validation error: ${error instanceof Error ? error.message : 'Unknown error'}`, }; } } /** * Sanitize string input to prevent XSS and injection attacks */ static sanitizeString(input) { return input .replace(/[<>]/g, '') // Remove potential HTML tags .replace(/javascript:/gi, '') // Remove javascript: protocol .replace(/on\w+\s*=/gi, '') // Remove event handlers .replace(/\0/g, '') // Remove null bytes .trim(); } /** * Sanitize object properties recursively */ static sanitizeObject(obj) { if (obj === null || typeof obj !== 'object') { return obj; } if (Array.isArray(obj)) { return obj.map(item => this.sanitizeObject(item)); } const sanitized = {}; for (const [key, value] of Object.entries(obj)) { // Sanitize key const cleanKey = this.sanitizeString(key); // Recursively sanitize value if (typeof value === 'string') { sanitized[cleanKey] = this.sanitizeString(value); } else if (typeof value === 'object') { sanitized[cleanKey] = this.sanitizeObject(value); } else { sanitized[cleanKey] = value; } } return sanitized; } /** * Check rate limiting for operations */ static checkRateLimit(identifier, operation) { const config = this.securityConfig.rateLimits[operation]; if (!config) { return { allowed: true }; // No rate limit configured } const now = Date.now(); const key = `${identifier}:${operation}`; const entry = this.rateLimitStore.get(key); // Initialize or reset window if (!entry || now - entry.windowStart >= config.windowMs) { this.rateLimitStore.set(key, { count: 1, windowStart: now }); return { allowed: true, remaining: config.maxRequests - 1, resetTime: now + config.windowMs, }; } // Check if limit exceeded if (entry.count >= config.maxRequests) { return { allowed: false, remaining: 0, resetTime: entry.windowStart + config.windowMs, }; } // Increment counter entry.count++; this.rateLimitStore.set(key, entry); return { allowed: true, remaining: config.maxRequests - entry.count, resetTime: entry.windowStart + config.windowMs, }; } /** * Validate method is allowed */ static isMethodAllowed(method) { return this.securityConfig.allowedMethods.includes(method); } /** * Generate cryptographically secure ID */ static generateSecureId(prefix) { const randomBytes = crypto.randomBytes(16); const id = randomBytes.toString('hex'); return prefix ? `${prefix}_${id}` : id; } /** * Generate secure session token */ static generateSessionToken() { return crypto.randomBytes(32).toString('base64url'); } /** * Validate quiz ID format for security */ static validateQuizId(quizId) { if (!quizId || typeof quizId !== 'string') { return { isValid: false, error: 'Quiz ID must be a non-empty string' }; } // Allow alphanumeric, hyphens, underscores const validPattern = /^[a-zA-Z0-9_-]+$/; if (!validPattern.test(quizId)) { return { isValid: false, error: 'Quiz ID contains invalid characters' }; } if (quizId.length > 50) { return { isValid: false, error: 'Quiz ID too long (max 50 characters)' }; } return { isValid: true }; } /** * Sanitize filename for secure file operations */ static sanitizeFilename(filename) { return filename .replace(/[^a-zA-Z0-9._-]/g, '') // Only allow alphanumeric, dots, underscores, hyphens .replace(/\.{2,}/g, '.') // Prevent directory traversal .substring(0, 255); // Limit length } /** * Clean up old rate limit entries */ static cleanupRateLimitStore() { const now = Date.now(); for (const [key, entry] of this.rateLimitStore.entries()) { const maxWindow = Math.max(...Object.values(this.securityConfig.rateLimits).map(c => c.windowMs)); if (now - entry.windowStart > maxWindow) { this.rateLimitStore.delete(key); } } } /** * Update security configuration */ static updateSecurityConfig(updates) { this.securityConfig = { ...this.securityConfig, ...updates }; } /** * Get current security configuration */ static getSecurityConfig() { return { ...this.securityConfig }; } /** * Hash sensitive data for logging */ static hashForLogging(data) { return crypto.createHash('sha256').update(data).digest('hex').substring(0, 8); } } exports.SecurityUtils = SecurityUtils; SecurityUtils.rateLimitStore = new Map(); SecurityUtils.securityConfig = { maxInputLength: 10000, allowedMethods: [ 'initialize', 'tools/list', 'tools/call', 'resources/list', 'resources/read', 'prompts/list', 'prompts/get', ], rateLimits: { tool_call: { windowMs: 60000, maxRequests: 100 }, resource_read: { windowMs: 60000, maxRequests: 200 }, quiz_create: { windowMs: 300000, maxRequests: 10 }, }, enableInputSanitization: true, requireSecureIds: true, }; // Cleanup rate limit store every 5 minutes setInterval(() => { SecurityUtils.cleanupRateLimitStore(); }, 5 * 60 * 1000);