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.

527 lines (526 loc) • 19.7 kB
"use strict"; /** * @fileoverview Sandbox Manager - Tool Execution Isolation System * @version 1.0.0 * @since 2025-08-02 * @lastUpdated 2025-08-02 * @module SandboxManager * @description Comprehensive sandboxing system for MCP tool execution with resource limits, * isolation, and security controls to prevent cross-tool contamination * @contributors Claude Code Agent * @dependencies vm, worker_threads, crypto, fs/promises * @requirements SECURITY_001 (Tool Execution Isolation) * @testCoverage Integration tests for sandbox boundaries and resource limits */ 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.sandboxManager = exports.SandboxManager = exports.SecurityViolationType = void 0; const crypto = __importStar(require("crypto")); const events_1 = require("events"); const path = __importStar(require("path")); const vm = __importStar(require("vm")); /** * Security violation types */ var SecurityViolationType; (function (SecurityViolationType) { SecurityViolationType["RESOURCE_LIMIT_EXCEEDED"] = "RESOURCE_LIMIT_EXCEEDED"; SecurityViolationType["UNAUTHORIZED_FILE_ACCESS"] = "UNAUTHORIZED_FILE_ACCESS"; SecurityViolationType["UNAUTHORIZED_NETWORK_ACCESS"] = "UNAUTHORIZED_NETWORK_ACCESS"; SecurityViolationType["UNAUTHORIZED_SUBPROCESS"] = "UNAUTHORIZED_SUBPROCESS"; SecurityViolationType["CAPABILITY_VIOLATION"] = "CAPABILITY_VIOLATION"; })(SecurityViolationType || (exports.SecurityViolationType = SecurityViolationType = {})); /** * Default resource limits */ const DEFAULT_RESOURCE_LIMITS = { maxCpuTime: 30000, // 30 seconds maxMemory: 100 * 1024 * 1024, // 100MB maxFileOps: 100, maxNetworkRequests: 10, maxExecutionTime: 30000, // 30 seconds }; /** * Default security capabilities for quiz tools */ const DEFAULT_QUIZ_CAPABILITIES = { allowDatabase: true, allowFileSystem: false, allowNetwork: false, allowSubprocess: false, allowedPaths: [], allowedHosts: [], }; /** * Default security capabilities for web tools */ const DEFAULT_WEB_CAPABILITIES = { allowDatabase: false, allowFileSystem: true, allowNetwork: true, allowSubprocess: false, allowedPaths: ['/tmp', '/var/log'], allowedHosts: ['localhost', '127.0.0.1'], }; /** * Comprehensive sandbox manager for tool execution isolation */ class SandboxManager extends events_1.EventEmitter { constructor() { super(); this.activeExecutions = new Map(); this.violationHistory = []; this.resourceMonitorInterval = null; this.startResourceMonitoring(); } /** * Execute a tool in an isolated sandbox environment */ async executeTool(toolName, toolHandler, args, customLimits, customCapabilities) { const executionId = this.generateExecutionId(); const limits = { ...DEFAULT_RESOURCE_LIMITS, ...customLimits }; const capabilities = this.getCapabilitiesForTool(toolName, customCapabilities); const context = { executionId, toolName, limits, capabilities, startTime: Date.now(), usage: { cpuTime: 0, memoryUsed: 0, fileOps: 0, networkRequests: 0, }, }; this.activeExecutions.set(executionId, context); try { this.emit('executionStarted', { executionId, toolName }); // Choose execution strategy based on tool requirements const result = await this.executeInIsolation(toolHandler, args, context); const executionTime = Date.now() - context.startTime; this.emit('executionCompleted', { executionId, toolName, executionTime, resourceUsage: context.usage, }); return { result, metadata: { executionId, executionTime, resourceUsage: context.usage, securityViolations: [], performanceMetrics: { memoryPeak: context.usage.memoryUsed, cpuPeak: context.usage.cpuTime, }, }, }; } catch (error) { this.handleExecutionError(executionId, error); throw error; } finally { this.activeExecutions.delete(executionId); } } /** * Execute tool in VM-based isolation */ async executeInIsolation(toolHandler, args, context) { return new Promise((resolve, reject) => { // Create isolated VM context const sandbox = this.createSecureSandbox(context); const vmContext = vm.createContext(sandbox); // Set up execution timeout const timeout = setTimeout(() => { this.recordViolation({ type: SecurityViolationType.RESOURCE_LIMIT_EXCEEDED, executionId: context.executionId, toolName: context.toolName, description: `Execution timeout exceeded: ${context.limits.maxExecutionTime}ms`, timestamp: Date.now(), severity: 'high', }); reject(new Error('Execution timeout exceeded')); }, context.limits.maxExecutionTime); try { // Wrap tool handler with resource monitoring const monitoredHandler = this.wrapWithResourceMonitoring(toolHandler, context); // Execute in VM context with limited scope const script = new vm.Script(` (async function() { try { return await toolHandler(args); } catch (error) { throw error; } })() `); // Add handler and args to sandbox sandbox.toolHandler = monitoredHandler; sandbox.args = args; // Execute with resource limits const result = script.runInContext(vmContext, { timeout: context.limits.maxExecutionTime, breakOnSigint: true, }); // Handle promise result if (result && typeof result.then === 'function') { result.then(resolve).catch(reject); } else { resolve(result); } } catch (error) { reject(error); } finally { clearTimeout(timeout); } }); } /** * Create secure sandbox environment */ createSecureSandbox(context) { const sandbox = { // Provide limited Node.js APIs based on capabilities console: { log: (...args) => console.log(`[${context.executionId}]`, ...args), error: (...args) => console.error(`[${context.executionId}]`, ...args), }, setTimeout, clearTimeout, setInterval, clearInterval, Promise, JSON, Math, Date, // Restricted global objects process: { env: {}, // Empty environment for security nextTick: process.nextTick, }, // Custom require function with capability checks require: this.createSecureRequire(context), }; // Add database access if allowed if (context.capabilities.allowDatabase) { sandbox.database = this.createSecureDatabaseAccess(context); } // Add file system access if allowed if (context.capabilities.allowFileSystem) { sandbox.fs = this.createSecureFileSystemAccess(context); } // Add network access if allowed if (context.capabilities.allowNetwork) { sandbox.http = this.createSecureNetworkAccess(context); sandbox.https = this.createSecureNetworkAccess(context); } return sandbox; } /** * Create secure require function with capability restrictions */ createSecureRequire(context) { const allowedModules = new Set(['crypto', 'util', 'path', 'url', 'querystring', 'uuid']); return (moduleId) => { if (!allowedModules.has(moduleId)) { this.recordViolation({ type: SecurityViolationType.CAPABILITY_VIOLATION, executionId: context.executionId, toolName: context.toolName, description: `Attempted to require unauthorized module: ${moduleId}`, timestamp: Date.now(), severity: 'medium', }); throw new Error(`Module '${moduleId}' is not allowed in sandbox`); } return require(moduleId); }; } /** * Create secure database access wrapper */ createSecureDatabaseAccess(context) { return { // Placeholder for secure database wrapper // This would create isolated database connections per tool query: async (sql, params) => { context.usage.fileOps++; if (context.usage.fileOps > context.limits.maxFileOps) { throw new Error('File operation limit exceeded'); } // Database query implementation with isolation return []; }, }; } /** * Create secure file system access wrapper */ createSecureFileSystemAccess(context) { return { readFile: async (filepath) => { if (!this.isPathAllowed(filepath, context.capabilities.allowedPaths)) { this.recordViolation({ type: SecurityViolationType.UNAUTHORIZED_FILE_ACCESS, executionId: context.executionId, toolName: context.toolName, description: `Attempted to access unauthorized path: ${filepath}`, timestamp: Date.now(), severity: 'high', }); throw new Error(`Access denied to path: ${filepath}`); } context.usage.fileOps++; if (context.usage.fileOps > context.limits.maxFileOps) { throw new Error('File operation limit exceeded'); } // Secure file access implementation return ''; }, }; } /** * Create secure network access wrapper */ createSecureNetworkAccess(context) { return { request: async (url, options) => { const hostname = new URL(url).hostname; if (!context.capabilities.allowedHosts.includes(hostname)) { this.recordViolation({ type: SecurityViolationType.UNAUTHORIZED_NETWORK_ACCESS, executionId: context.executionId, toolName: context.toolName, description: `Attempted to access unauthorized host: ${hostname}`, timestamp: Date.now(), severity: 'high', }); throw new Error(`Access denied to host: ${hostname}`); } context.usage.networkRequests++; if (context.usage.networkRequests > context.limits.maxNetworkRequests) { throw new Error('Network request limit exceeded'); } // Secure network request implementation return {}; }, }; } /** * Wrap tool handler with resource monitoring */ wrapWithResourceMonitoring(handler, context) { return async (...args) => { const startMemory = process.memoryUsage().heapUsed; const startTime = process.hrtime.bigint(); try { const result = await handler(...args); // Update resource usage const endTime = process.hrtime.bigint(); const endMemory = process.memoryUsage().heapUsed; context.usage.cpuTime += Number(endTime - startTime) / 1000000; // Convert to ms context.usage.memoryUsed = Math.max(context.usage.memoryUsed, endMemory - startMemory); // Check limits this.checkResourceLimits(context); return result; } catch (error) { this.handleExecutionError(context.executionId, error); throw error; } }; } /** * Check if resource limits are exceeded */ checkResourceLimits(context) { if (context.usage.cpuTime > context.limits.maxCpuTime) { this.recordViolation({ type: SecurityViolationType.RESOURCE_LIMIT_EXCEEDED, executionId: context.executionId, toolName: context.toolName, description: `CPU time limit exceeded: ${context.usage.cpuTime}ms > ${context.limits.maxCpuTime}ms`, timestamp: Date.now(), severity: 'high', }); throw new Error('CPU time limit exceeded'); } if (context.usage.memoryUsed > context.limits.maxMemory) { this.recordViolation({ type: SecurityViolationType.RESOURCE_LIMIT_EXCEEDED, executionId: context.executionId, toolName: context.toolName, description: `Memory limit exceeded: ${context.usage.memoryUsed} bytes > ${context.limits.maxMemory} bytes`, timestamp: Date.now(), severity: 'high', }); throw new Error('Memory limit exceeded'); } } /** * Get security capabilities for a specific tool */ getCapabilitiesForTool(toolName, customCapabilities) { let baseCapabilities; if (toolName.startsWith('create_quiz') || toolName.startsWith('get_quiz') || toolName.startsWith('list_quizzes') || toolName.startsWith('delete_quiz')) { baseCapabilities = { ...DEFAULT_QUIZ_CAPABILITIES }; } else if (toolName.includes('server_management') || toolName.includes('web')) { baseCapabilities = { ...DEFAULT_WEB_CAPABILITIES }; } else { // Default restrictive capabilities baseCapabilities = { allowDatabase: false, allowFileSystem: false, allowNetwork: false, allowSubprocess: false, allowedPaths: [], allowedHosts: [], }; } return { ...baseCapabilities, ...customCapabilities }; } /** * Check if file path is allowed */ isPathAllowed(filepath, allowedPaths) { const normalizedPath = path.normalize(filepath); return allowedPaths.some(allowedPath => normalizedPath.startsWith(path.normalize(allowedPath))); } /** * Generate unique execution ID */ generateExecutionId() { return `exec_${Date.now()}_${crypto.randomBytes(8).toString('hex')}`; } /** * Record security violation */ recordViolation(violation) { this.violationHistory.push(violation); this.emit('securityViolation', violation); // Log critical violations if (violation.severity === 'critical') { console.error(`🚨 CRITICAL SECURITY VIOLATION: ${violation.description}`, violation); } } /** * Handle execution errors */ handleExecutionError(executionId, error) { this.emit('executionError', { executionId, error }); console.error(`Execution error in ${executionId}:`, error); } /** * Start resource monitoring */ startResourceMonitoring() { this.resourceMonitorInterval = setInterval(() => { for (const [executionId, context] of this.activeExecutions) { const currentTime = Date.now(); const executionTime = currentTime - context.startTime; if (executionTime > context.limits.maxExecutionTime) { this.recordViolation({ type: SecurityViolationType.RESOURCE_LIMIT_EXCEEDED, executionId, toolName: context.toolName, description: `Execution time exceeded: ${executionTime}ms`, timestamp: currentTime, severity: 'high', }); } } }, 1000); // Check every second } /** * Stop resource monitoring */ stopResourceMonitoring() { if (this.resourceMonitorInterval) { clearInterval(this.resourceMonitorInterval); this.resourceMonitorInterval = null; } } /** * Get active executions count */ getActiveExecutionsCount() { return this.activeExecutions.size; } /** * Get security violation history */ getViolationHistory() { return [...this.violationHistory]; } /** * Get execution statistics */ getExecutionStats() { const violationsByType = {}; for (const violation of this.violationHistory) { violationsByType[violation.type] = (violationsByType[violation.type] || 0) + 1; } return { activeExecutions: this.activeExecutions.size, totalViolations: this.violationHistory.length, violationsByType, }; } /** * Cleanup resources */ cleanup() { this.stopResourceMonitoring(); this.activeExecutions.clear(); this.removeAllListeners(); } } exports.SandboxManager = SandboxManager; // Export singleton instance exports.sandboxManager = new SandboxManager();