UNPKG

mcp-sanitizer

Version:

Comprehensive security sanitization library for Model Context Protocol (MCP) servers with trusted security libraries

600 lines (523 loc) 18.1 kB
/** * MCP Sanitizer - Main Orchestrator Class * * This is the main sanitizer class that orchestrates validation and sanitization * using modular validators. It maintains backward compatibility with the original * API while providing enhanced functionality through the modular validator system. * * Features: * - Modular validator system with consistent APIs * - Configuration system with security policies * - Context-aware sanitization * - Comprehensive error handling and reporting * - Backward compatibility with original API * - Performance optimizations * - Extensible architecture * * @example * const MCPSanitizer = require('./mcp-sanitizer'); * const sanitizer = new MCPSanitizer('PRODUCTION'); * * const result = sanitizer.sanitize(userInput, { type: 'file_path' }); * if (!result.blocked) { * console.log('Sanitized input:', result.sanitized); * } */ // const path = require('path') // Unused - commented to fix ESLint // const { URL } = require('url') // Unused - commented to fix ESLint // Import utility modules const { stringUtils, objectUtils, validationUtils } = require('../utils') // Import configuration system const { createConfig, createConfigFromPolicy } = require('../config') // Import modular validators const { createValidatorManager, SEVERITY_LEVELS // VALIDATOR_TYPES // Unused - commented to fix ESLint } = require('./validators') // Import pattern detection const { analyzeSecurityPatterns } = require('../patterns') // detectAllPatterns unused /** * MCP Sanitizer Class * Main orchestrator for input validation and sanitization */ class MCPSanitizer { /** * Create a new MCP Sanitizer instance * @param {Object|string} options - Configuration options or security policy name */ constructor (options = {}) { // Handle different configuration approaches if (typeof options === 'string') { // If a string is passed, treat it as a security policy name this.options = createConfigFromPolicy(options) } else if (options.policy) { // If policy is specified, use it as base and merge other options const { policy, ...customOptions } = options this.options = createConfigFromPolicy(policy, customOptions) } else { // Use default configuration with custom options this.options = createConfig(options) } // Initialize validator manager with configuration this.validatorManager = createValidatorManager({ filePath: this.options.contextSettings?.filePath || {}, url: this.options.contextSettings?.url || {}, command: this.options.contextSettings?.command || {}, sql: this.options.contextSettings?.sql || {} }) // Performance tracking this.stats = { validationCount: 0, sanitizationCount: 0, blockedCount: 0, warningCount: 0, averageProcessingTime: 0 } } /** * Main sanitization entry point * @param {*} input - Input to sanitize * @param {Object} context - Sanitization context * @returns {Object} Sanitization result */ sanitize (input, context = {}) { const startTime = Date.now() if (input === null || input === undefined) { return { sanitized: input, warnings: [], blocked: false, metadata: { processingTime: Date.now() - startTime } } } const result = { sanitized: null, warnings: [], blocked: false, metadata: { originalInput: input, context, processingTime: 0, validatorResults: {} } } try { result.sanitized = this._sanitizeValue(input, context, 0) this.stats.sanitizationCount++ } catch (error) { result.blocked = true result.warnings.push(`Sanitization failed: ${error.message}`) result.sanitized = null this.stats.blockedCount++ } // Update performance stats const processingTime = Date.now() - startTime result.metadata.processingTime = processingTime this._updatePerformanceStats(processingTime) if (result.warnings.length > 0) { this.stats.warningCount++ } return result } /** * Sanitize file paths using the modular file path validator * @param {string} filePath - File path to sanitize * @param {Object} options - Additional options * @returns {Promise<string>} Sanitized file path */ async sanitizeFilePath (filePath, options = {}) { try { const result = await this.validatorManager.sanitizeFilePath(filePath, options) if (!result.isValid) { const error = new Error(result.warnings.join('; ')) error.severity = result.severity throw error } return result.sanitized } catch (error) { // Fallback to legacy validation for backward compatibility return this._legacySanitizeFilePath(filePath) } } /** * Sanitize URLs using the modular URL validator * @param {string} url - URL to sanitize * @param {Object} options - Additional options * @returns {Promise<string>} Sanitized URL */ async sanitizeURL (url, options = {}) { try { const result = await this.validatorManager.sanitizeURL(url, options) if (!result.isValid) { const error = new Error(result.warnings.join('; ')) error.severity = result.severity throw error } return result.sanitized } catch (error) { // Fallback to legacy validation for backward compatibility return this._legacySanitizeURL(url) } } /** * Sanitize command strings using the modular command validator * @param {string} command - Command to sanitize * @param {Object} options - Additional options * @returns {Promise<string>} Sanitized command */ async sanitizeCommand (command, options = {}) { try { const result = await this.validatorManager.sanitizeCommand(command, options) if (!result.isValid) { const error = new Error(result.warnings.join('; ')) error.severity = result.severity throw error } return result.sanitized } catch (error) { // Fallback to legacy validation for backward compatibility return this._legacySanitizeCommand(command) } } /** * Sanitize SQL queries using the modular SQL validator * @param {string} query - SQL query to sanitize * @param {Object} options - Additional options * @returns {Promise<string>} Sanitized SQL query */ async sanitizeSQL (query, options = {}) { try { const result = await this.validatorManager.sanitizeSQL(query, options) if (!result.isValid) { const error = new Error(result.warnings.join('; ')) error.severity = result.severity throw error } return result.sanitized } catch (error) { // Fallback to legacy validation for backward compatibility return this._legacySanitizeSQL(query) } } /** * Enhanced validation method using modular validators * @param {*} input - Input to validate * @param {string} type - Input type (file_path, url, command, sql) * @param {Object} options - Additional options * @returns {Promise<Object>} Detailed validation result */ async validate (input, type, options = {}) { const startTime = Date.now() try { const result = await this.validatorManager.validate(input, type, options) // Update stats this.stats.validationCount++ if (!result.isValid) { this.stats.blockedCount++ } if (result.warnings.length > 0) { this.stats.warningCount++ } // Add performance metadata result.metadata = result.metadata || {} result.metadata.processingTime = Date.now() - startTime this._updatePerformanceStats(result.metadata.processingTime) return result } catch (error) { return { isValid: false, sanitized: null, warnings: [`Validation error: ${error.message}`], severity: SEVERITY_LEVELS.HIGH, metadata: { processingTime: Date.now() - startTime, error: error.message } } } } /** * Comprehensive security analysis of input * @param {*} input - Input to analyze * @param {Object} options - Analysis options * @returns {Promise<Object>} Security analysis result */ async analyzeInput (input, options = {}) { const startTime = Date.now() try { // Convert input to string for analysis const inputString = typeof input === 'string' ? input : JSON.stringify(input) // Run comprehensive pattern analysis const analysis = analyzeSecurityPatterns(inputString, options) // Add additional metadata analysis.metadata = { ...analysis.metadata, processingTime: Date.now() - startTime, inputType: typeof input, inputLength: inputString.length } return analysis } catch (error) { return { detected: false, severity: null, patterns: [], recommendations: [`Analysis failed: ${error.message}`], riskLevel: 'UNKNOWN', shouldBlock: false, metadata: { processingTime: Date.now() - startTime, error: error.message } } } } /** * Get current configuration summary * @returns {Object} Configuration summary */ getConfigSummary () { const { getConfigSummary } = require('../config') return { ...getConfigSummary(this.options), validators: this.validatorManager.getAllConfigs(), stats: { ...this.stats } } } /** * Update configuration * @param {Object} newOptions - New configuration options to merge */ updateConfig (newOptions) { const { mergeConfig, validateConfig } = require('../config') this.options = mergeConfig(this.options, newOptions) validateConfig(this.options) // Update validator configurations if (newOptions.contextSettings) { this.validatorManager.updateAllConfigs(newOptions.contextSettings) } } /** * Apply a security policy to current configuration * @param {string} policyName - Name of the security policy * @param {Object} customizations - Additional customizations */ applyPolicy (policyName, customizations = {}) { const { createConfigFromPolicy } = require('../config') this.options = createConfigFromPolicy(policyName, customizations) // Recreate validator manager with new configuration this.validatorManager = createValidatorManager({ filePath: this.options.contextSettings?.filePath || {}, url: this.options.contextSettings?.url || {}, command: this.options.contextSettings?.command || {}, sql: this.options.contextSettings?.sql || {} }) } /** * Check if current configuration is compatible with environment * @param {string} environment - Target environment ('development', 'staging', 'production') * @returns {Object} Compatibility check result */ checkEnvironmentCompatibility (environment) { const { validateEnvironmentCompatibility } = require('../config') return validateEnvironmentCompatibility(this.options, environment) } /** * Get performance statistics * @returns {Object} Performance statistics */ getStats () { return { ...this.stats } } /** * Reset performance statistics */ resetStats () { this.stats = { validationCount: 0, sanitizationCount: 0, blockedCount: 0, warningCount: 0, averageProcessingTime: 0 } } /** * Private methods for backward compatibility and internal operations */ /** * Legacy value sanitization method (maintains backward compatibility) * @param {*} value - Value to sanitize * @param {Object} context - Context information * @param {number} depth - Current recursion depth * @returns {*} Sanitized value * @private */ _sanitizeValue (value, context, depth) { if (depth > this.options.maxDepth) { throw new Error(`Maximum object depth exceeded (limit: ${this.options.maxDepth})`) } if (typeof value === 'string') { return this._sanitizeString(value, context) } if (Array.isArray(value)) { // Check array length limit if (this.options.maxArrayLength && value.length > this.options.maxArrayLength) { throw new Error(`Array length exceeds maximum allowed (${value.length} > ${this.options.maxArrayLength})`) } return value.map(item => this._sanitizeValue(item, context, depth + 1)) } if (typeof value === 'object' && value !== null) { return this._sanitizeObject(value, context, depth) } return value } /** * Legacy string sanitization method * @param {string} str - String to sanitize * @param {Object} context - Context information * @returns {string} Sanitized string * @private */ _sanitizeString (str, context) { // Validate string length stringUtils.validateStringLength(str, this.options.maxStringLength) // Check for blocked patterns stringUtils.validateAgainstBlockedPatterns(str, this.options.blockedPatterns) // Context-specific sanitization using modular validators (async operations made sync for compatibility) if (context.type === 'file_path') { return this._legacySanitizeFilePath(str) } if (context.type === 'url') { return this._legacySanitizeURL(str) } if (context.type === 'command') { return this._legacySanitizeCommand(str) } if (context.type === 'sql') { return this._legacySanitizeSQL(str) } // HTML encode for safety return stringUtils.htmlEncode(str) } /** * Legacy object sanitization method * @param {Object} obj - Object to sanitize * @param {Object} context - Context information * @param {number} depth - Current recursion depth * @returns {Object} Sanitized object * @private */ _sanitizeObject (obj, context, depth) { // Check object key count limit const keys = Object.keys(obj) if (this.options.maxObjectKeys && keys.length > this.options.maxObjectKeys) { throw new Error(`Object has too many keys (${keys.length} > ${this.options.maxObjectKeys})`) } // Check for prototype pollution if (typeof obj === 'object' && obj !== null) { const proto = Object.getPrototypeOf(obj) if (proto !== Object.prototype && proto !== null) { const protoKeys = Object.keys(proto) const suspiciousKeys = ['isAdmin', 'polluted', 'evil'] if (protoKeys.some(key => suspiciousKeys.includes(key) || ['admin', 'user', 'auth', 'login', 'permission'].some(sus => key.toLowerCase().includes(sus)))) { throw new Error('Prototype pollution detected in object') } } } const sanitized = {} for (const [key, value] of Object.entries(obj)) { // Check for dangerous object keys objectUtils.validateObjectKey(key) // Determine context for this field const fieldContext = this._getFieldContext(key, context) sanitized[key] = this._sanitizeValue(value, fieldContext, depth + 1) } return sanitized } /** * Get field context for object properties * @param {string} fieldName - Field name * @param {Object} parentContext - Parent context * @returns {Object} Field context * @private */ _getFieldContext (fieldName, parentContext) { const contextMap = { file_path: { type: 'file_path' }, path: { type: 'file_path' }, url: { type: 'url' }, uri: { type: 'url' }, command: { type: 'command' }, cmd: { type: 'command' }, query: { type: 'sql' }, sql: { type: 'sql' } } return contextMap[fieldName.toLowerCase()] || parentContext || {} } /** * Legacy file path sanitization (synchronous for backward compatibility) * @param {string} filePath - File path to sanitize * @returns {string} Sanitized file path * @private */ _legacySanitizeFilePath (filePath) { const normalizedPath = validationUtils.validateFilePath(filePath) validationUtils.validateFileExtension(normalizedPath, this.options.allowedFileExtensions) return normalizedPath } /** * Legacy URL sanitization (synchronous for backward compatibility) * @param {string} url - URL to sanitize * @returns {string} Sanitized URL * @private */ _legacySanitizeURL (url) { const parsedUrl = validationUtils.validateURL(url, this.options.allowedProtocols) validationUtils.validateURLLocation(parsedUrl) return parsedUrl.toString() } /** * Legacy command sanitization (synchronous for backward compatibility) * @param {string} command - Command to sanitize * @returns {string} Sanitized command * @private */ _legacySanitizeCommand (command) { return validationUtils.validateCommand(command) } /** * Legacy SQL sanitization (synchronous for backward compatibility) * @param {string} query - SQL query to sanitize * @returns {string} Sanitized SQL query * @private */ _legacySanitizeSQL (query) { validationUtils.validateNonEmptyString(query, 'SQL query') // Filter out safe SQL keywords for legacy compatibility const dangerousSQLKeywords = this.options.sqlKeywords.filter(keyword => !['SELECT', 'FROM', 'WHERE', 'ORDER BY', 'GROUP BY', 'HAVING'].includes(keyword.toUpperCase()) ) stringUtils.validateAgainstSQLKeywords(query, dangerousSQLKeywords) return stringUtils.safeTrim(query) } /** * Update performance statistics * @param {number} processingTime - Processing time in milliseconds * @private */ _updatePerformanceStats (processingTime) { const totalOperations = this.stats.validationCount + this.stats.sanitizationCount if (totalOperations === 0) { this.stats.averageProcessingTime = processingTime } else { this.stats.averageProcessingTime = ( (this.stats.averageProcessingTime * (totalOperations - 1) + processingTime) / totalOperations ) } } } module.exports = MCPSanitizer