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
JavaScript
;
/**
* @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();