mcp-sanitizer
Version:
Comprehensive security sanitization library for Model Context Protocol (MCP) servers with trusted security libraries
753 lines (672 loc) • 21.2 kB
JavaScript
/**
* NoSQL Injection Pattern Detection
*
* Comprehensive detection for NoSQL database injection attacks including:
* - MongoDB operators and JavaScript injection
* - CouchDB Mango query selectors
* - Redis command injection
* - Cassandra CQL injection
* - Server-side JavaScript (SSJS) attacks
*
* Designed to achieve >95% protection rate against NoSQL injection vectors
* with <5ms performance overhead.
*
* Based on OWASP NoSQL injection prevention guidelines and real-world
* attack patterns from security research.
*/
/**
* Severity levels for NoSQL injection patterns
*/
const SEVERITY_LEVELS = {
CRITICAL: 'critical',
HIGH: 'high',
MEDIUM: 'medium',
LOW: 'low'
};
/**
* NoSQL database types and their specific patterns
*/
const NOSQL_TYPES = {
MONGODB: 'mongodb',
COUCHDB: 'couchdb',
REDIS: 'redis',
CASSANDRA: 'cassandra',
ELASTICSEARCH: 'elasticsearch'
};
/**
* MongoDB operators that can be exploited for injection
* Based on MongoDB 6.0+ operator reference
*/
const MONGODB_OPERATORS = {
// Comparison operators
COMPARISON: [
'$eq', '$gt', '$gte', '$in', '$lt', '$lte', '$ne', '$nin'
],
// Logical operators
LOGICAL: [
'$and', '$not', '$nor', '$or'
],
// Element operators
ELEMENT: [
'$exists', '$type'
],
// Evaluation operators (high risk)
EVALUATION: [
'$expr', '$jsonSchema', '$mod', '$regex', '$text', '$where'
],
// Geospatial operators
GEOSPATIAL: [
'$geoIntersects', '$geoWithin', '$near', '$nearSphere'
],
// Array operators
ARRAY: [
'$all', '$elemMatch', '$size'
],
// Bitwise operators
BITWISE: [
'$bitsAllClear', '$bitsAllSet', '$bitsAnyClear', '$bitsAnySet'
],
// Update operators
UPDATE: [
'$inc', '$mul', '$rename', '$setOnInsert', '$set', '$unset', '$min', '$max',
'$addToSet', '$pop', '$pullAll', '$pull', '$pushAll', '$push', '$each'
]
};
/**
* JavaScript injection patterns commonly used in MongoDB $where clauses
*/
const JAVASCRIPT_INJECTION_PATTERNS = [
// Sleep/delay functions
/sleep\s*\(/i,
/setTimeout\s*\(/i,
/setInterval\s*\(/i,
// Date-based timing attacks
/new\s+Date\s*\(\)/i,
/Date\s*\(\s*\)/i,
/getTime\s*\(\s*\)/i,
// Function execution
/Function\s*\(/i,
/eval\s*\(/i,
/setTimeout\s*\(/i,
// Database operations
/db\./i,
/collection\./i,
/find\s*\(/i,
/insert\s*\(/i,
/update\s*\(/i,
/delete\s*\(/i,
/remove\s*\(/i,
// System access
/process\./i,
/require\s*\(/i,
/import\s+/i,
// Network operations
/XMLHttpRequest/i,
/fetch\s*\(/i,
/http\./i,
// File system access
/fs\./i,
/readFile/i,
/writeFile/i,
/exec\s*\(/i
];
/**
* Redis command injection patterns
*/
const REDIS_COMMANDS = [
// Dangerous commands
'FLUSHDB', 'FLUSHALL', 'CONFIG', 'EVAL', 'EVALSHA', 'SCRIPT',
'DEBUG', 'SHUTDOWN', 'SLAVEOF', 'REPLICAOF', 'MIGRATE', 'RESTORE',
// Data manipulation
'DEL', 'RENAME', 'RENAMENX', 'EXPIRE', 'EXPIREAT', 'PERSIST',
'MOVE', 'SORT', 'SCAN', 'KEYS',
// Pub/Sub (potential for command injection)
'PUBLISH', 'SUBSCRIBE', 'PSUBSCRIBE', 'PUNSUBSCRIBE', 'UNSUBSCRIBE',
// Lua scripting
'EVAL', 'EVALSHA', 'SCRIPT LOAD', 'SCRIPT EXISTS', 'SCRIPT FLUSH', 'SCRIPT KILL'
];
/**
* CouchDB query operators and injection patterns
*/
const COUCHDB_OPERATORS = [
'$lt', '$lte', '$eq', '$ne', '$gte', '$gt', '$exists', '$type', '$in', '$nin',
'$size', '$mod', '$regex', '$elemMatch', '$allMatch', '$keyMapMatch'
];
/**
* Cassandra CQL injection patterns
*/
const CASSANDRA_PATTERNS = [
/ALLOW\s+FILTERING/i,
/TOKEN\s*\(/i,
/BATCH\s+/i,
/TRUNCATE\s+/i,
/DROP\s+/i,
/ALTER\s+/i,
/CREATE\s+/i,
/USING\s+TTL/i,
/IF\s+NOT\s+EXISTS/i
];
/**
* Advanced NoSQL injection detection class
*/
class NoSQLValidator {
constructor (options = {}) {
this.options = {
strictMode: options.strictMode || false,
maxDepth: options.maxDepth || 10,
maxKeys: options.maxKeys || 100,
enableJavaScriptDetection: options.enableJavaScriptDetection !== false,
enableOperatorDetection: options.enableOperatorDetection !== false,
enableCommandDetection: options.enableCommandDetection !== false,
...options
};
this.operatorCache = new Map();
this.patternCache = new Map();
}
/**
* Detect NoSQL injection patterns in input
* @param {string|Object} input - Input to analyze
* @param {Object} options - Detection options
* @returns {Object} Detection result
*/
detect (input, options = {}) {
const startTime = process.hrtime.bigint();
try {
const result = this._performDetection(input, options);
const endTime = process.hrtime.bigint();
result.performance = {
detectionTime: Number(endTime - startTime) / 1000000, // Convert to milliseconds
cacheHits: this.operatorCache.size + this.patternCache.size
};
return result;
} catch (error) {
return {
detected: false,
error: error.message,
severity: null,
patterns: [],
performance: {
detectionTime: Number(process.hrtime.bigint() - startTime) / 1000000,
error: true
}
};
}
}
/**
* Perform the actual NoSQL injection detection
* @private
*/
_performDetection (input, options) {
const mergedOptions = { ...this.options, ...options };
const result = {
detected: false,
severity: null,
patterns: [],
vulnerabilities: [],
nosqlType: null,
injectionVectors: []
};
// Handle different input types
if (typeof input === 'string') {
this._detectInString(input, result, mergedOptions);
} else if (typeof input === 'object' && input !== null) {
this._detectInObject(input, result, mergedOptions);
}
// Determine overall severity and detection status
if (result.patterns.length > 0) {
result.detected = true;
result.severity = this._calculateSeverity(result.vulnerabilities);
}
return result;
}
/**
* Detect NoSQL injection in string inputs
* @private
*/
_detectInString (input, result, options) {
input.toLowerCase().trim();
// Check for JSON-like structures
if (this._isJsonLike(input)) {
try {
const parsed = JSON.parse(input);
this._detectInObject(parsed, result, options);
return;
} catch (e) {
// Continue with string analysis if JSON parsing fails
}
}
// MongoDB JavaScript injection detection
if (options.enableJavaScriptDetection) {
this._detectJavaScriptInjection(input, result);
}
// Redis command injection detection
if (options.enableCommandDetection) {
this._detectRedisCommands(input, result);
}
// Cassandra CQL injection detection
this._detectCassandraInjection(input, result);
// Generic NoSQL operator detection in strings
this._detectOperatorsInString(input, result);
}
/**
* Detect NoSQL injection in object inputs
* @private
*/
_detectInObject (obj, result, options, currentDepth = 0) {
if (currentDepth > options.maxDepth) {
result.patterns.push('depth_limit_exceeded');
result.vulnerabilities.push({
type: 'depth_limit_exceeded',
severity: SEVERITY_LEVELS.MEDIUM,
description: `Object depth exceeds maximum allowed (${options.maxDepth})`
});
// Still check for operators at this level before stopping
}
const keys = Object.keys(obj);
if (keys.length > options.maxKeys) {
result.vulnerabilities.push({
type: 'key_limit_exceeded',
severity: SEVERITY_LEVELS.MEDIUM,
description: `Object has too many keys (${keys.length} > ${options.maxKeys})`
});
}
for (const key of keys) {
// Check if key is a NoSQL operator
if (this._isNoSQLOperator(key)) {
const vulnerability = this._analyzeOperator(key, obj[key]);
result.patterns.push(key);
result.vulnerabilities.push(vulnerability);
if (!result.nosqlType) {
result.nosqlType = this._identifyNoSQLType(key);
}
}
// Recursively check nested objects
const value = obj[key];
if (typeof value === 'object' && value !== null) {
if (Array.isArray(value)) {
for (let i = 0; i < value.length; i++) {
if (typeof value[i] === 'object' && value[i] !== null) {
// Only recurse if we haven't exceeded depth limit
if (currentDepth <= options.maxDepth) {
this._detectInObject(value[i], result, options, currentDepth + 1);
}
} else if (typeof value[i] === 'string') {
this._detectInString(value[i], result, options);
}
}
} else {
// Only recurse if we haven't exceeded depth limit
if (currentDepth <= options.maxDepth) {
this._detectInObject(value, result, options, currentDepth + 1);
}
}
} else if (typeof value === 'string') {
this._detectInString(value, result, options);
}
}
}
/**
* Check if a key is a NoSQL operator
* @private
*/
_isNoSQLOperator (key) {
if (this.operatorCache.has(key)) {
return this.operatorCache.get(key);
}
const isOperator = key.startsWith('$') ||
COUCHDB_OPERATORS.includes(key) ||
this._isRedisCommand(key);
this.operatorCache.set(key, isOperator);
return isOperator;
}
/**
* Analyze a specific NoSQL operator for security risks
* @private
*/
_analyzeOperator (operator, value) {
const vulnerability = {
operator,
value,
type: 'nosql_operator',
severity: SEVERITY_LEVELS.MEDIUM,
description: `NoSQL operator detected: ${operator}`
};
// High-risk operators
if (['$where', '$expr', '$function'].includes(operator)) {
vulnerability.severity = SEVERITY_LEVELS.CRITICAL;
vulnerability.description = `Critical NoSQL operator allowing code execution: ${operator}`;
vulnerability.codeExecution = true;
}
// Regex operators (potential ReDoS)
if (operator === '$regex') {
vulnerability.severity = SEVERITY_LEVELS.HIGH;
vulnerability.description = 'Regex operator detected - potential for ReDoS attacks';
vulnerability.redosRisk = this._analyzeRegexComplexity(value);
}
// Authentication bypass operators
if (['$ne', '$gt', '$gte', '$exists'].includes(operator)) {
vulnerability.severity = SEVERITY_LEVELS.HIGH;
vulnerability.description = `Authentication bypass operator detected: ${operator}`;
vulnerability.authBypass = true;
}
// JavaScript in $where clauses
if (operator === '$where' && typeof value === 'string') {
const jsVulnerability = this._analyzeJavaScriptCode(value);
if (jsVulnerability) {
vulnerability.severity = SEVERITY_LEVELS.CRITICAL;
vulnerability.javascriptInjection = jsVulnerability;
}
}
return vulnerability;
}
/**
* Detect JavaScript injection patterns
* @private
*/
_detectJavaScriptInjection (input, result) {
for (const pattern of JAVASCRIPT_INJECTION_PATTERNS) {
if (pattern.test(input)) {
result.patterns.push(`javascript_injection:${pattern.source}`);
result.vulnerabilities.push({
type: 'javascript_injection',
severity: SEVERITY_LEVELS.CRITICAL,
pattern: pattern.source,
description: 'JavaScript injection pattern detected',
codeExecution: true
});
}
}
}
/**
* Detect Redis command injection
* @private
*/
_detectRedisCommands (input, result) {
const upperInput = input.toUpperCase();
for (const command of REDIS_COMMANDS) {
if (upperInput.includes(command)) {
result.patterns.push(`redis_command:${command}`);
result.vulnerabilities.push({
type: 'redis_command_injection',
command,
severity: this._getRedisCommandSeverity(command),
description: `Dangerous Redis command detected: ${command}`
});
result.nosqlType = NOSQL_TYPES.REDIS;
}
}
// Check for EVAL with Lua code
if (/EVAL\s+["'][^"']*["']/i.test(input)) {
result.patterns.push('redis_eval_injection');
result.vulnerabilities.push({
type: 'redis_lua_injection',
severity: SEVERITY_LEVELS.CRITICAL,
description: 'Redis EVAL command with Lua code injection detected',
codeExecution: true
});
}
}
/**
* Detect Cassandra CQL injection
* @private
*/
_detectCassandraInjection (input, result) {
for (const pattern of CASSANDRA_PATTERNS) {
if (pattern.test(input)) {
result.patterns.push(`cassandra_cql:${pattern.source}`);
result.vulnerabilities.push({
type: 'cassandra_cql_injection',
pattern: pattern.source,
severity: SEVERITY_LEVELS.HIGH,
description: 'Cassandra CQL injection pattern detected'
});
result.nosqlType = NOSQL_TYPES.CASSANDRA;
}
}
}
/**
* Detect NoSQL operators in string format
* @private
*/
_detectOperatorsInString (input, result) {
// Check for MongoDB operators in string format
const operatorPattern = /\$(?:where|regex|gt|gte|lt|lte|ne|eq|in|nin|exists|type|mod|text|search|or|and|not|nor|elemMatch|size|all|expr|jsonSchema)/gi;
const matches = input.match(operatorPattern);
if (matches) {
for (const match of matches) {
result.patterns.push(`string_operator:${match}`);
result.vulnerabilities.push({
type: 'nosql_operator_in_string',
operator: match,
severity: this._getOperatorSeverity(match),
description: `NoSQL operator found in string: ${match}`
});
}
result.nosqlType = NOSQL_TYPES.MONGODB;
}
// Check for boolean injection patterns
const booleanInjectionPatterns = [
/true,\s*\$where:/i,
/false,\s*\$where:/i,
/['"]\s*,\s*\$where:/i,
/admin['"]\s*,\s*\$where:/i,
/return\s+true\s*;?\s*\/\//i,
/\|\|\s*true/i
];
for (const pattern of booleanInjectionPatterns) {
if (pattern.test(input)) {
result.patterns.push(`boolean_injection:${pattern.source}`);
result.vulnerabilities.push({
type: 'boolean_injection',
severity: SEVERITY_LEVELS.HIGH,
pattern: pattern.source,
description: 'Boolean NoSQL injection pattern detected'
});
}
}
// Check for SSJS patterns
const ssjsPatterns = [
/['"]\s*;\s*var\s+\w+\s*=/i,
/['"]\s*;\s*this\./i,
/db\.\w+\.(drop|remove|insert)/i,
/this\.constructor\.constructor/i,
/process\(\)\.exit/i,
/['"]\s*;\s*.*?;\s*\/\//i
];
for (const pattern of ssjsPatterns) {
if (pattern.test(input)) {
result.patterns.push(`ssjs_injection:${pattern.source}`);
result.vulnerabilities.push({
type: 'server_side_js_injection',
severity: SEVERITY_LEVELS.CRITICAL,
pattern: pattern.source,
description: 'Server-side JavaScript injection pattern detected',
codeExecution: true
});
}
}
}
/**
* Analyze JavaScript code for dangerous patterns
* @private
*/
_analyzeJavaScriptCode (code) {
const dangerousPatterns = [
{ pattern: /sleep|timeout|delay/i, risk: 'timing_attack' },
{ pattern: /eval|Function|require/i, risk: 'code_execution' },
{ pattern: /db\.|collection\./i, risk: 'database_access' },
{ pattern: /process\.|fs\.|http\./i, risk: 'system_access' }
];
const risks = [];
for (const { pattern, risk } of dangerousPatterns) {
if (pattern.test(code)) {
risks.push(risk);
}
}
return risks.length > 0 ? risks : null;
}
/**
* Analyze regex complexity for ReDoS potential
* @private
*/
_analyzeRegexComplexity (regex) {
if (typeof regex !== 'string') return false;
// Check for nested quantifiers and alternation (ReDoS indicators)
const redosPatterns = [
/\([^)]*\*[^)]*\*[^)]*\)/, // Nested quantifiers
/\([^)]*\+[^)]*\+[^)]*\)/, // Multiple plus quantifiers
/\|.*\|.*\|/, // Multiple alternations
/\([^)]*\*[^)]*\|[^)]*\*[^)]*\)/, // Alternation with quantifiers
/\(\.\*\+\)/, // Potential ReDoS pattern like (.*)+
/\(\.\+\*\)/, // Another ReDoS pattern
/\(\w\+\)\+/, // Word character repetition
/\(\S\+\)\+/, // Non-space repetition
/\(\.\+\)\+/, // Classic catastrophic backtracking pattern
/\(\.\*\)\+/, // Another backtracking pattern
/\(a\+\)\+b/, // Example from test case
/\([^)]*\+[^)]*\+[^)]*\)/ // Complex nested quantifiers
];
return redosPatterns.some(pattern => pattern.test(regex));
}
/**
* Identify NoSQL database type based on operators
* @private
*/
_identifyNoSQLType (operator) {
if (operator.startsWith('$')) {
return NOSQL_TYPES.MONGODB;
}
if (COUCHDB_OPERATORS.includes(operator)) {
return NOSQL_TYPES.COUCHDB;
}
if (this._isRedisCommand(operator)) {
return NOSQL_TYPES.REDIS;
}
return null;
}
/**
* Check if string is a Redis command
* @private
*/
_isRedisCommand (str) {
return REDIS_COMMANDS.includes(str.toUpperCase());
}
/**
* Get severity level for Redis commands
* @private
*/
_getRedisCommandSeverity (command) {
const criticalCommands = ['FLUSHALL', 'FLUSHDB', 'SHUTDOWN', 'CONFIG', 'EVAL', 'EVALSHA', 'DEBUG'];
const highCommands = ['DEL', 'RENAME', 'SCRIPT', 'MIGRATE', 'RESTORE'];
if (criticalCommands.includes(command)) {
return SEVERITY_LEVELS.CRITICAL;
}
if (highCommands.includes(command)) {
return SEVERITY_LEVELS.HIGH;
}
return SEVERITY_LEVELS.MEDIUM;
}
/**
* Get severity level for NoSQL operators
* @private
*/
_getOperatorSeverity (operator) {
const criticalOps = ['$where', '$expr', '$function'];
const highOps = ['$regex', '$ne', '$gt', '$gte', '$exists'];
if (criticalOps.includes(operator)) {
return SEVERITY_LEVELS.CRITICAL;
}
if (highOps.includes(operator)) {
return SEVERITY_LEVELS.HIGH;
}
return SEVERITY_LEVELS.MEDIUM;
}
/**
* Calculate overall severity from vulnerabilities
* @private
*/
_calculateSeverity (vulnerabilities) {
if (vulnerabilities.some(v => v.severity === SEVERITY_LEVELS.CRITICAL)) {
return SEVERITY_LEVELS.CRITICAL;
}
if (vulnerabilities.some(v => v.severity === SEVERITY_LEVELS.HIGH)) {
return SEVERITY_LEVELS.HIGH;
}
if (vulnerabilities.some(v => v.severity === SEVERITY_LEVELS.MEDIUM)) {
return SEVERITY_LEVELS.MEDIUM;
}
return SEVERITY_LEVELS.LOW;
}
/**
* Check if string looks like JSON
* @private
*/
_isJsonLike (str) {
const trimmed = str.trim();
return (trimmed.startsWith('{') && trimmed.endsWith('}')) ||
(trimmed.startsWith('[') && trimmed.endsWith(']'));
}
}
/**
* Main detection function - detects NoSQL injection patterns
* @param {string|Object} input - Input to analyze
* @param {Object} options - Detection options
* @returns {Object} Detection result with patterns and severity
*/
function detectNoSQLInjection (input, options = {}) {
const validator = new NoSQLValidator(options);
return validator.detect(input, options);
}
/**
* Simple boolean check for NoSQL injection
* @param {string|Object} input - Input to check
* @param {Object} options - Detection options
* @returns {boolean} True if NoSQL injection patterns are detected
*/
function hasNoSQLInjection (input, options = {}) {
return detectNoSQLInjection(input, options).detected;
}
/**
* Check for specific NoSQL database type injection
* @param {string|Object} input - Input to check
* @param {string} dbType - Database type to check for
* @returns {boolean} True if specific DB injection is detected
*/
function hasNoSQLInjectionForDB (input, dbType) {
const result = detectNoSQLInjection(input);
return result.detected && result.nosqlType === dbType;
}
/**
* Get all MongoDB operators that could be used for injection
* @returns {Array} Array of MongoDB operators
*/
function getMongoDBOperators () {
return Object.values(MONGODB_OPERATORS).flat();
}
/**
* Performance-optimized bulk detection
* @param {Array} inputs - Array of inputs to check
* @param {Object} options - Detection options
* @returns {Array} Array of detection results
*/
function detectBulkNoSQLInjection (inputs, options = {}) {
const validator = new NoSQLValidator(options);
return inputs.map(input => validator.detect(input, options));
}
module.exports = {
// Main detection functions
detectNoSQLInjection,
hasNoSQLInjection,
hasNoSQLInjectionForDB,
detectBulkNoSQLInjection,
// Utility functions
getMongoDBOperators,
// Classes for advanced usage
NoSQLValidator,
// Constants
SEVERITY_LEVELS,
NOSQL_TYPES,
MONGODB_OPERATORS,
REDIS_COMMANDS,
COUCHDB_OPERATORS
};