mcp-sanitizer
Version:
Comprehensive security sanitization library for Model Context Protocol (MCP) servers with trusted security libraries
500 lines (425 loc) • 16.7 kB
JavaScript
/**
* Comprehensive NoSQL Injection Detection Tests
*
* Tests all NoSQL injection vectors to ensure >95% protection rate
* Covers MongoDB, CouchDB, Redis, Cassandra, and JavaScript injection
*/
const {
detectNoSQLInjection,
hasNoSQLInjection,
hasNoSQLInjectionForDB,
detectBulkNoSQLInjection,
NoSQLValidator,
SEVERITY_LEVELS,
NOSQL_TYPES,
getMongoDBOperators
} = require('../src/patterns/nosql-injection');
describe('NoSQL Injection Detection', () => {
// let validator // Unused - tests use direct function imports
beforeEach(() => {
// validator = new NoSQLValidator() // Unused
});
describe('MongoDB Injection Detection', () => {
test('should detect $where JavaScript injection', () => {
const maliciousQuery = '{"$where": "this.username == this.password"}';
const result = detectNoSQLInjection(maliciousQuery);
expect(result.detected).toBe(true);
expect(result.severity).toBe(SEVERITY_LEVELS.CRITICAL);
expect(result.nosqlType).toBe(NOSQL_TYPES.MONGODB);
expect(result.vulnerabilities.some(v => v.codeExecution)).toBe(true);
});
test('should detect authentication bypass operators', () => {
const testCases = [
'{"username": {"$ne": null}, "password": {"$ne": null}}',
'{"username": {"$gt": ""}, "password": {"$gt": ""}}',
'{"username": {"$exists": true}, "password": {"$exists": true}}'
];
testCases.forEach(query => {
const result = detectNoSQLInjection(query);
expect(result.detected).toBe(true);
expect(result.severity).toBe(SEVERITY_LEVELS.HIGH);
expect(result.vulnerabilities.some(v => v.authBypass)).toBe(true);
});
});
test('should detect regex injection with ReDoS potential', () => {
const regexQuery = '{"username": {"$regex": ".*"}, "password": {"$regex": ".*"}}';
const result = detectNoSQLInjection(regexQuery);
expect(result.detected).toBe(true);
expect(result.severity).toBe(SEVERITY_LEVELS.HIGH);
expect(result.vulnerabilities.some(v => v.type === 'nosql_operator')).toBe(true);
});
test('should detect complex ReDoS patterns', () => {
const complexRegex = '{"field": {"$regex": "(a+)+b"}}';
const result = detectNoSQLInjection(complexRegex);
expect(result.detected).toBe(true);
const regexVuln = result.vulnerabilities.find(v => v.operator === '$regex');
expect(regexVuln.redosRisk).toBe(true);
});
test('should detect JavaScript sleep/timing attacks', () => {
const timingAttacks = [
'{"$where": "sleep(5000) || true"}',
'{"$where": "function() { var d = new Date(); while ((new Date() - d) < 5000); return true; }"}',
'{"$where": "setTimeout(function(){}, 5000) || true"}'
];
timingAttacks.forEach(attack => {
const result = detectNoSQLInjection(attack);
expect(result.detected).toBe(true);
expect(result.severity).toBe(SEVERITY_LEVELS.CRITICAL);
});
});
test('should detect logical operator injection', () => {
const logicalQueries = [
'{"$or": [{"username": "admin"}, {"role": "admin"}]}',
'{"$and": [{"active": true}, {"$where": "1==1"}]}',
'{"$nor": [{"deleted": true}]}'
];
logicalQueries.forEach(query => {
const result = detectNoSQLInjection(query);
expect(result.detected).toBe(true);
});
});
test('should detect array operator injection', () => {
const arrayQueries = [
'{"username": {"$in": ["admin", "root", "administrator"]}}',
'{"permissions": {"$all": ["read", "write", "admin"]}}',
'{"tags": {"$size": 0}}'
];
arrayQueries.forEach(query => {
const result = detectNoSQLInjection(query);
expect(result.detected).toBe(true);
});
});
test('should detect evaluation operators', () => {
const evalQueries = [
'{"$expr": {"$eq": ["$username", "$password"]}}',
'{"age": {"$mod": [10, 0]}}',
'{"$text": {"$search": "malicious"}}'
];
evalQueries.forEach(query => {
const result = detectNoSQLInjection(query);
expect(result.detected).toBe(true);
});
});
});
describe('CouchDB Injection Detection', () => {
test('should detect CouchDB selector injection', () => {
const couchQuery = '{"selector": {"_id": {"$gt": null}}}';
const result = detectNoSQLInjection(couchQuery);
expect(result.detected).toBe(true);
expect(result.nosqlType).toBe(NOSQL_TYPES.MONGODB); // CouchDB uses similar operators
});
test('should detect CouchDB Mango query patterns', () => {
const mangoQueries = [
'{"selector": {"username": {"$regex": ".*"}}}',
'{"selector": {"type": {"$exists": true}}}',
'{"selector": {"$or": [{"role": "admin"}]}}'
];
mangoQueries.forEach(query => {
const result = detectNoSQLInjection(query);
expect(result.detected).toBe(true);
});
});
});
describe('Redis Injection Detection', () => {
test('should detect dangerous Redis commands', () => {
const redisCommands = [
'EVAL "redis.call(\'flushall\')" 0',
'EVAL "return redis.call(\'get\', \'sensitive_key\')" 0',
'FLUSHALL',
'CONFIG SET',
'SCRIPT LOAD "return redis.call(\'auth\', \'password\')"'
];
redisCommands.forEach(command => {
const result = detectNoSQLInjection(command);
expect(result.detected).toBe(true);
expect(result.nosqlType).toBe(NOSQL_TYPES.REDIS);
expect(result.severity).toBeOneOf([SEVERITY_LEVELS.HIGH, SEVERITY_LEVELS.CRITICAL]);
});
});
test('should detect Lua script injection', () => {
const luaInjection = 'EVAL "local result = redis.call(\'keys\', \'*\'); return result" 0';
const result = detectNoSQLInjection(luaInjection);
expect(result.detected).toBe(true);
expect(result.vulnerabilities.some(v => v.codeExecution)).toBe(true);
});
test('should detect Redis pub/sub injection', () => {
const pubsubCommands = [
'PUBLISH sensitive-channel "malicious data"',
'SUBSCRIBE admin-channel',
'PSUBSCRIBE *'
];
pubsubCommands.forEach(command => {
const result = detectNoSQLInjection(command);
expect(result.detected).toBe(true);
});
});
});
describe('Cassandra CQL Injection Detection', () => {
test('should detect dangerous CQL patterns', () => {
const cqlQueries = [
'SELECT * FROM users WHERE token(id) > token(?)',
'SELECT * FROM users ALLOW FILTERING',
'TRUNCATE users',
'DROP TABLE sensitive_data',
'ALTER TABLE users ADD COLUMN admin boolean'
];
cqlQueries.forEach(query => {
const result = detectNoSQLInjection(query);
expect(result.detected).toBe(true);
expect(result.nosqlType).toBe(NOSQL_TYPES.CASSANDRA);
});
});
test('should detect CQL batch injection', () => {
const batchQuery = `
BEGIN BATCH
INSERT INTO users (id, name) VALUES (1, 'admin');
UPDATE users SET role = 'admin' WHERE id = 1;
APPLY BATCH;
`;
const result = detectNoSQLInjection(batchQuery);
expect(result.detected).toBe(true);
});
});
describe('JavaScript Injection in NoSQL', () => {
test('should detect dangerous JavaScript patterns', () => {
const jsPatterns = [
'function() { require("child_process").exec("rm -rf /"); }',
'function() { process.exit(1); }',
'function() { eval("malicious code"); }',
'this.constructor.constructor("return process")().exit()',
'function() { global.process.mainModule.require("fs").readFileSync("/etc/passwd"); }'
];
jsPatterns.forEach(pattern => {
const query = `{"$where": "${pattern}"}`;
const result = detectNoSQLInjection(query);
expect(result.detected).toBe(true);
expect(result.severity).toBe(SEVERITY_LEVELS.CRITICAL);
});
});
test('should detect Server-Side JavaScript (SSJS) injection', () => {
const ssjsPatterns = [
'"; var date = new Date(); do { curDate = new Date(); } while(curDate-date<5000); "',
'\'; db.users.drop(); //',
'"; this.constructor.constructor("return process")().exit(); //'
];
ssjsPatterns.forEach(pattern => {
const result = detectNoSQLInjection(pattern);
expect(result.detected).toBe(true);
});
});
});
describe('Nested Object Detection', () => {
test('should detect injection in deeply nested objects', () => {
const nestedQuery = {
user: {
profile: {
settings: {
query: {
$where: 'this.password === "admin"'
}
}
}
}
};
const result = detectNoSQLInjection(nestedQuery);
expect(result.detected).toBe(true);
});
test('should handle maximum depth limits', () => {
// Create deeply nested object
let deepObject = { $where: 'malicious' };
for (let i = 0; i < 15; i++) {
deepObject = { nested: deepObject };
}
const result = detectNoSQLInjection(deepObject, { maxDepth: 10 });
expect(result.detected).toBe(true);
expect(result.vulnerabilities.some(v => v.type === 'depth_limit_exceeded')).toBe(true);
});
test('should detect operators in arrays', () => {
const arrayQuery = {
$or: [
{ username: 'admin' },
{ $where: 'true' },
{ role: { $ne: null } }
]
};
const result = detectNoSQLInjection(arrayQuery);
expect(result.detected).toBe(true);
expect(result.patterns.length).toBeGreaterThan(2);
});
});
describe('String-based NoSQL Detection', () => {
test('should detect operators in string format', () => {
const stringQueries = [
'username[$ne]=null&password[$ne]=null',
'user[$where]=this.username==this.password',
'filter[$regex]=.*admin.*',
'sort[$exists]=true'
];
stringQueries.forEach(query => {
const result = detectNoSQLInjection(query);
expect(result.detected).toBe(true);
});
});
test('should detect boolean injection attempts', () => {
const booleanInjections = [
'true, $where: \'1 == 1\'',
'admin\', $where: \'1 == 1\', a: \'a',
'false; return true; //'
];
booleanInjections.forEach(injection => {
const result = detectNoSQLInjection(injection);
expect(result.detected).toBe(true);
});
});
});
describe('Performance and Edge Cases', () => {
test('should process large objects within time limit', () => {
const largeObject = {};
for (let i = 0; i < 50; i++) {
largeObject[`field${i}`] = { value: `data${i}`, nested: { more: 'data' } };
}
largeObject.$where = 'malicious';
const result = detectNoSQLInjection(largeObject);
expect(result.detected).toBe(true);
expect(result.performance.detectionTime).toBeLessThan(5); // <5ms requirement
});
test('should handle malformed JSON gracefully', () => {
const malformedInputs = [
'{"$where": function() { return true; }', // Missing closing brace
'{$where: "this.username"', // Invalid JSON
'{"$where":}', // Missing value
null,
undefined,
''
];
malformedInputs.forEach(input => {
const result = detectNoSQLInjection(input);
expect(result).toBeDefined();
// Should not throw errors
});
});
test('should detect mixed injection types', () => {
const mixedQuery = {
mongodb: { $where: 'sleep(5000)' },
redis: 'EVAL "redis.call(\'flushall\')" 0',
cassandra: 'SELECT * FROM users ALLOW FILTERING'
};
const result = detectNoSQLInjection(mixedQuery);
expect(result.detected).toBe(true);
expect(result.patterns.length).toBeGreaterThan(3);
});
});
describe('Utility Functions', () => {
test('hasNoSQLInjection should return boolean', () => {
expect(hasNoSQLInjection('{"$where": "true"}')).toBe(true);
expect(hasNoSQLInjection('{"username": "admin"}')).toBe(false);
});
test('hasNoSQLInjectionForDB should detect specific database types', () => {
expect(hasNoSQLInjectionForDB('{"$where": "true"}', NOSQL_TYPES.MONGODB)).toBe(true);
expect(hasNoSQLInjectionForDB('FLUSHALL', NOSQL_TYPES.REDIS)).toBe(true);
expect(hasNoSQLInjectionForDB('ALLOW FILTERING', NOSQL_TYPES.CASSANDRA)).toBe(true);
});
test('detectBulkNoSQLInjection should process arrays', () => {
const inputs = [
'{"$where": "true"}',
'{"username": "admin"}',
'FLUSHALL',
'SELECT * FROM users ALLOW FILTERING'
];
const results = detectBulkNoSQLInjection(inputs);
expect(results).toHaveLength(4);
expect(results.filter(r => r.detected)).toHaveLength(3);
});
test('getMongoDBOperators should return all operators', () => {
const operators = getMongoDBOperators();
expect(operators).toContain('$where');
expect(operators).toContain('$regex');
expect(operators).toContain('$ne');
expect(operators.length).toBeGreaterThan(40);
});
});
describe('Real-World Attack Scenarios', () => {
test('should detect authentication bypass scenario', () => {
// Common MongoDB authentication bypass
const loginBypass = {
username: { $ne: null },
password: { $ne: null }
};
const result = detectNoSQLInjection(loginBypass);
expect(result.detected).toBe(true);
expect(result.severity).toBe(SEVERITY_LEVELS.HIGH);
expect(result.vulnerabilities.some(v => v.authBypass)).toBe(true);
});
test('should detect timing attack scenario', () => {
// MongoDB timing attack using $where
const timingAttack = {
$where: 'sleep(Math.floor(Math.random() * 1000) + 5000) || true'
};
const result = detectNoSQLInjection(timingAttack);
expect(result.detected).toBe(true);
expect(result.severity).toBe(SEVERITY_LEVELS.CRITICAL);
});
test('should detect data extraction scenario', () => {
// CouchDB data extraction
const dataExtraction = {
selector: {
_id: { $gt: null }
},
limit: 999999
};
const result = detectNoSQLInjection(dataExtraction);
expect(result.detected).toBe(true);
});
test('should detect DoS attack scenario', () => {
// Redis DoS attack
const dosAttack = 'EVAL "while true do end" 0';
const result = detectNoSQLInjection(dosAttack);
expect(result.detected).toBe(true);
expect(result.severity).toBe(SEVERITY_LEVELS.CRITICAL);
});
});
describe('Configuration Options', () => {
test('should respect strict mode', () => {
const strictValidator = new NoSQLValidator({ strictMode: true });
// In strict mode, even less dangerous operators should be flagged
const result = strictValidator.detect({ field: { $exists: true } });
expect(result.detected).toBe(true);
});
test('should respect disabled detection options', () => {
const limitedValidator = new NoSQLValidator({
enableJavaScriptDetection: false,
enableCommandDetection: false
});
const jsResult = limitedValidator.detect('{"$where": "sleep(5000)"}');
expect(jsResult.vulnerabilities.some(v => v.type === 'javascript_injection')).toBe(false);
const cmdResult = limitedValidator.detect('FLUSHALL');
expect(cmdResult.vulnerabilities.some(v => v.type === 'redis_command_injection')).toBe(false);
});
test('should handle custom max keys limit', () => {
const limitedValidator = new NoSQLValidator({ maxKeys: 5 });
const largeObject = {};
for (let i = 0; i < 10; i++) {
largeObject[`key${i}`] = 'value';
}
const result = limitedValidator.detect(largeObject);
expect(result.vulnerabilities.some(v => v.type === 'key_limit_exceeded')).toBe(true);
});
});
});
// Custom Jest matcher for severity levels
expect.extend({
toBeOneOf (received, array) {
const pass = array.includes(received);
if (pass) {
return {
message: () => `expected ${received} not to be one of ${array.join(', ')}`,
pass: true
};
} else {
return {
message: () => `expected ${received} to be one of ${array.join(', ')}`,
pass: false
};
}
}
});