UNPKG

mcp-sanitizer

Version:

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

464 lines (397 loc) 11.9 kB
/** * Prototype Pollution Pattern Detection Module * * Detects patterns commonly used in prototype pollution attacks, including * dangerous object keys, constructor manipulation, and prototype chain traversal. * * Based on security best practices from prototype pollution research, * OWASP guidelines, and common attack vectors in JavaScript applications. */ const SEVERITY_LEVELS = { CRITICAL: 'critical', HIGH: 'high', MEDIUM: 'medium', LOW: 'low' }; /** * Dangerous object keys that can lead to prototype pollution */ const DANGEROUS_KEYS = [ '__proto__', 'constructor', 'prototype', 'constructor.prototype', '__defineGetter__', '__defineSetter__', '__lookupGetter__', '__lookupSetter__', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'toString', 'valueOf' ]; /** * Patterns for prototype pollution in different contexts */ const POLLUTION_PATTERNS = [ // Direct prototype manipulation /__proto__\s*[=:]/g, /constructor\s*\.\s*prototype\s*[=:]/g, /prototype\s*\.\s*constructor\s*[=:]/g, // Bracket notation access /\[\s*['"]__proto__['"]\s*\]/g, /\[\s*['"]constructor['"]\s*\]/g, /\[\s*['"]prototype['"]\s*\]/g, // Function constructor manipulation /constructor\s*\.\s*constructor\s*[=:]/g, /Function\s*\.\s*prototype\s*[=:]/g, /Object\s*\.\s*prototype\s*[=:]/g, // Array and object prototype pollution /Array\s*\.\s*prototype\s*\.\s*\w+\s*[=:]/g, /Object\s*\.\s*prototype\s*\.\s*\w+\s*[=:]/g, // Getter/setter manipulation /__defineGetter__\s*\(/g, /__defineSetter__\s*\(/g, /Object\s*\.\s*defineProperty\s*\(\s*.*prototype/g ]; /** * JSON-based prototype pollution patterns */ const JSON_POLLUTION_PATTERNS = [ // JSON key patterns /"__proto__"\s*:/g, /"constructor"\s*:/g, /"prototype"\s*:/g, // Nested JSON pollution /"constructor"\s*:\s*{\s*"prototype"/g, /"__proto__"\s*:\s*{\s*"constructor"/g, // Unicode escaped versions /"\u005f\u005fproto\u005f\u005f"\s*:/g, /"\u0063onstructor"\s*:/g, // Hex escaped versions /"\x5f\x5fproto\x5f\x5f"\s*:/g, /"\x63onstructor"\s*:/g ]; /** * Lodash-specific pollution patterns */ const LODASH_POLLUTION_PATTERNS = [ // Path-based pollution via lodash /lodash.*set.*__proto__/gi, /lodash.*merge.*__proto__/gi, /lodash.*defaults.*__proto__/gi, /_.set.*__proto__/g, /_.merge.*__proto__/g, /_.defaults.*__proto__/g, // Property path pollution /\[\s*'__proto__\..*'\s*\]/g, /\[\s*"__proto__\..*"\s*\]/g ]; /** * Express.js specific pollution patterns */ const EXPRESS_POLLUTION_PATTERNS = [ // Body parser pollution /req\.body\.__proto__/g, /req\.query\.__proto__/g, /req\.params\.__proto__/g, // URL-encoded pollution /__proto__\[.*\]/g, /constructor\[prototype\]/g, /constructor\.prototype\[/g ]; /** * Encoding bypass patterns for prototype pollution */ const ENCODING_BYPASS_PATTERNS = [ // URL encoding /%5f%5fproto%5f%5f/gi, // __proto__ /%63onstructor/gi, // constructor /%70rototype/gi, // prototype // Unicode encoding /\\u005f\\u005fproto\\u005f\\u005f/gi, /\\u0063onstructor/gi, /\\u0070rototype/gi, // Hex encoding /\\x5f\\x5fproto\\x5f\\x5f/gi, /\\x63onstructor/gi, /\\x70rototype/gi, // Mixed case bypass /__PROTO__/gi, /CONSTRUCTOR/gi, /PROTOTYPE/gi ]; /** * Property access patterns that might indicate pollution */ const PROPERTY_ACCESS_PATTERNS = [ // Dynamic property access /\[\s*.*proto.*\s*\]/gi, /\[\s*.*constructor.*\s*\]/gi, // Template literal access /\$\{.*__proto__.*\}/g, /\$\{.*constructor.*\}/g, // Computed property names /\[\s*['"]\w*proto\w*['"]\s*\]/gi, /\[\s*['"]\w*constructor\w*['"]\s*\]/gi ]; /** * Main detection function for prototype pollution patterns * @param {string|Object} input - The input to analyze (string or object) * @param {Object} options - Detection options * @returns {Object} Detection result with severity and details */ function detectPrototypePollution (input, options = {}) { const detectedPatterns = []; let maxSeverity = null; // Handle different input types if (typeof input === 'object' && input !== null) { const objectResult = checkObjectKeys(input); if (objectResult.detected) { detectedPatterns.push(...objectResult.patterns); maxSeverity = getHigherSeverity(maxSeverity, objectResult.severity); } } if (typeof input === 'string') { // Check pollution patterns const pollutionResult = checkPollutionPatterns(input); if (pollutionResult.detected) { detectedPatterns.push(...pollutionResult.patterns); maxSeverity = getHigherSeverity(maxSeverity, pollutionResult.severity); } // Check JSON pollution patterns const jsonResult = checkJSONPollutionPatterns(input); if (jsonResult.detected) { detectedPatterns.push(...jsonResult.patterns); maxSeverity = getHigherSeverity(maxSeverity, jsonResult.severity); } // Check lodash patterns const lodashResult = checkLodashPollutionPatterns(input); if (lodashResult.detected) { detectedPatterns.push(...lodashResult.patterns); maxSeverity = getHigherSeverity(maxSeverity, lodashResult.severity); } // Check Express patterns const expressResult = checkExpressPollutionPatterns(input); if (expressResult.detected) { detectedPatterns.push(...expressResult.patterns); maxSeverity = getHigherSeverity(maxSeverity, expressResult.severity); } // Check encoding bypass patterns const encodingResult = checkEncodingBypassPatterns(input); if (encodingResult.detected) { detectedPatterns.push(...encodingResult.patterns); maxSeverity = getHigherSeverity(maxSeverity, encodingResult.severity); } // Check property access patterns const propertyResult = checkPropertyAccessPatterns(input); if (propertyResult.detected) { detectedPatterns.push(...propertyResult.patterns); maxSeverity = getHigherSeverity(maxSeverity, propertyResult.severity); } } return { detected: detectedPatterns.length > 0, severity: maxSeverity, patterns: detectedPatterns, message: detectedPatterns.length > 0 ? `Prototype pollution patterns detected: ${detectedPatterns.join(', ')}` : null }; } /** * Check object keys for dangerous properties */ function checkObjectKeys (obj, prefix = '', visited = new WeakSet()) { if (visited.has(obj)) { return { detected: false, severity: null, patterns: [] }; } visited.add(obj); const detected = []; try { for (const key of Object.keys(obj)) { const fullKey = prefix ? `${prefix}.${key}` : key; // Check if key is dangerous if (DANGEROUS_KEYS.includes(key)) { detected.push(`dangerous_key:${fullKey}`); } // Check for prototype pollution key patterns if (key.includes('__proto__') || key.includes('constructor') || key.includes('prototype')) { detected.push(`pollution_key:${fullKey}`); } // Recursively check nested objects if (typeof obj[key] === 'object' && obj[key] !== null) { const nestedResult = checkObjectKeys(obj[key], fullKey, visited); if (nestedResult.detected) { detected.push(...nestedResult.patterns); } } } } catch (error) { // Handle cases where object properties cannot be enumerated detected.push(`object_enumeration_error:${error.message}`); } return { detected: detected.length > 0, severity: detected.length > 0 ? SEVERITY_LEVELS.CRITICAL : null, patterns: detected }; } /** * Check for prototype pollution patterns in strings */ function checkPollutionPatterns (input) { const detected = []; for (const pattern of POLLUTION_PATTERNS) { if (pattern.test(input)) { detected.push(`pollution_pattern:${pattern.source}`); } } return { detected: detected.length > 0, severity: detected.length > 0 ? SEVERITY_LEVELS.CRITICAL : null, patterns: detected }; } /** * Check for JSON-based pollution patterns */ function checkJSONPollutionPatterns (input) { const detected = []; for (const pattern of JSON_POLLUTION_PATTERNS) { if (pattern.test(input)) { detected.push(`json_pollution:${pattern.source}`); } } return { detected: detected.length > 0, severity: detected.length > 0 ? SEVERITY_LEVELS.HIGH : null, patterns: detected }; } /** * Check for Lodash-specific pollution patterns */ function checkLodashPollutionPatterns (input) { const detected = []; for (const pattern of LODASH_POLLUTION_PATTERNS) { if (pattern.test(input)) { detected.push(`lodash_pollution:${pattern.source}`); } } return { detected: detected.length > 0, severity: detected.length > 0 ? SEVERITY_LEVELS.HIGH : null, patterns: detected }; } /** * Check for Express.js specific pollution patterns */ function checkExpressPollutionPatterns (input) { const detected = []; for (const pattern of EXPRESS_POLLUTION_PATTERNS) { if (pattern.test(input)) { detected.push(`express_pollution:${pattern.source}`); } } return { detected: detected.length > 0, severity: detected.length > 0 ? SEVERITY_LEVELS.HIGH : null, patterns: detected }; } /** * Check for encoding bypass patterns */ function checkEncodingBypassPatterns (input) { const detected = []; for (const pattern of ENCODING_BYPASS_PATTERNS) { if (pattern.test(input)) { detected.push(`encoding_bypass:${pattern.source}`); } } return { detected: detected.length > 0, severity: detected.length > 0 ? SEVERITY_LEVELS.MEDIUM : null, patterns: detected }; } /** * Check for suspicious property access patterns */ function checkPropertyAccessPatterns (input) { const detected = []; for (const pattern of PROPERTY_ACCESS_PATTERNS) { if (pattern.test(input)) { detected.push(`property_access:${pattern.source}`); } } return { detected: detected.length > 0, severity: detected.length > 0 ? SEVERITY_LEVELS.MEDIUM : null, patterns: detected }; } /** * Get the higher severity between two severity levels */ function getHigherSeverity (current, newSeverity) { if (!current) return newSeverity; if (!newSeverity) return current; const severityOrder = [ SEVERITY_LEVELS.LOW, SEVERITY_LEVELS.MEDIUM, SEVERITY_LEVELS.HIGH, SEVERITY_LEVELS.CRITICAL ]; const currentIndex = severityOrder.indexOf(current); const newIndex = severityOrder.indexOf(newSeverity); return newIndex > currentIndex ? newSeverity : current; } /** * Simple boolean check for prototype pollution * @param {string|Object} input - The input to check * @returns {boolean} True if prototype pollution patterns are detected */ function isPrototypePollution (input) { return detectPrototypePollution(input).detected; } /** * Check if an object key is dangerous for prototype pollution * @param {string} key - The object key to check * @returns {boolean} True if the key is dangerous */ function isDangerousKey (key) { return DANGEROUS_KEYS.includes(key) || key.includes('__proto__') || key.includes('constructor') || key.includes('prototype'); } module.exports = { // Main detection functions detectPrototypePollution, isPrototypePollution, isDangerousKey, // Individual checkers checkObjectKeys, checkPollutionPatterns, checkJSONPollutionPatterns, checkLodashPollutionPatterns, checkExpressPollutionPatterns, checkEncodingBypassPatterns, checkPropertyAccessPatterns, // Pattern exports for reuse DANGEROUS_KEYS, POLLUTION_PATTERNS, JSON_POLLUTION_PATTERNS, LODASH_POLLUTION_PATTERNS, EXPRESS_POLLUTION_PATTERNS, ENCODING_BYPASS_PATTERNS, PROPERTY_ACCESS_PATTERNS, // Constants SEVERITY_LEVELS };