mcp-sanitizer
Version:
Comprehensive security sanitization library for Model Context Protocol (MCP) servers with trusted security libraries
464 lines (397 loc) • 11.9 kB
JavaScript
/**
* 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
};