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