Skip to main content
Glama
exact-search-channel-security.test.ts9.75 kB
import { ExactSearchChannel } from '../../../../src/infrastructure/services/search/exact-search-channel.js'; import { Session } from 'neo4j-driver'; import { describe, it, expect, beforeEach, vi } from 'vitest'; describe('ExactSearchChannel Security Tests', () => { let channel: ExactSearchChannel; let mockSession: any; beforeEach(() => { mockSession = { run: vi.fn().mockResolvedValue({ records: [] }) }; channel = new ExactSearchChannel(mockSession as Session); }); describe('sanitizeLuceneQuery security through public API', () => { it('should escape all BASE85 special characters when searching', async () => { // Test with all BASE85 special characters const base85SpecialChars = '!#$%&()*+,-./:;=?@_{}~`'; await channel.search(base85SpecialChars, 10); // Check both FULLTEXT queries were called with properly escaped characters const calls = mockSession.run.mock.calls; // Should have at least 2 calls (metadata and content FULLTEXT searches) expect(calls.length).toBeGreaterThanOrEqual(2); // Find the FULLTEXT query calls const fulltextCalls = calls.filter((call: any[]) => call[0].includes('db.index.fulltext.queryNodes') ); expect(fulltextCalls.length).toBeGreaterThanOrEqual(2); // Check each FULLTEXT call has escaped query fulltextCalls.forEach((call: any[]) => { const params = call[1]; expect(params.query).toBeDefined(); // The query should only contain alphanumeric, spaces, and escaped special chars // Every special char from BASE85 should be preceded by backslash const query = params.query; // Check that no unescaped special chars remain // This regex finds any special char NOT preceded by backslash const unescapedSpecialChars = query.match(/(?<!\\)[!#$%&()*+,\-./:;=?@_{}~`]/g); expect(unescapedSpecialChars).toBeNull(); }); }); it('should prevent Lucene injection attacks', async () => { const maliciousQueries = [ 'field:value', // Field query injection 'name:* OR id:*', // OR injection 'test"; MATCH (n) DETACH DELETE n;//', // Cypher injection attempt 'test AND metadata:secret', // AND injection 'test NOT secure', // NOT injection 'test~0.8', // Fuzzy query injection 'test^2.0', // Boost injection '[a TO z]', // Range query injection 'test*', // Wildcard injection 'test?', // Single char wildcard ]; for (const malicious of maliciousQueries) { mockSession.run.mockClear(); await channel.search(malicious, 10); const calls = mockSession.run.mock.calls; const fulltextCalls = calls.filter((call: any[]) => call[0].includes('db.index.fulltext.queryNodes') ); fulltextCalls.forEach((call: any[]) => { const query = call[1].query; // Verify dangerous Lucene patterns are neutralized expect(query).not.toMatch(/[^\\]:/); // Unescaped colon (field queries) expect(query).not.toContain(' OR '); // Uppercase OR should be lowercase expect(query).not.toContain(' AND '); // Uppercase AND should be lowercase expect(query).not.toContain(' NOT '); // Uppercase NOT should be lowercase expect(query).not.toContain(' TO '); // Uppercase TO should be lowercase // Note: MATCH and DELETE are Cypher commands, not Lucene operators // They're harmless in FULLTEXT context but we include them in the query // Verify operators were converted to lowercase (safe) if (malicious.includes(' OR ')) expect(query).toContain(' or '); if (malicious.includes(' AND ')) expect(query).toContain(' and '); if (malicious.includes(' NOT ')) expect(query).toContain(' not '); // Special Lucene operators should be escaped if (query.includes('~')) expect(query).toContain('\\~'); if (query.includes('^')) expect(query).toContain('\\^'); if (query.includes('*')) expect(query).toContain('\\*'); if (query.includes('?')) expect(query).toContain('\\?'); if (query.includes(':')) expect(query).toContain('\\:'); if (query.includes('[')) expect(query).toContain('\\['); if (query.includes(']')) expect(query).toContain('\\]'); }); } }); it('should not double-escape already escaped characters', async () => { // Test with pre-escaped query const preEscaped = 'test\\:value\\*end'; await channel.search(preEscaped, 10); const calls = mockSession.run.mock.calls; const fulltextCalls = calls.filter((call: any[]) => call[0].includes('db.index.fulltext.queryNodes') ); fulltextCalls.forEach((call: any[]) => { const query = call[1].query; // Should maintain single escaping, not double expect(query).toContain('test\\:value\\*end'); expect(query).not.toContain('test\\\\:'); // No double backslash expect(query).not.toContain('\\\\*end'); }); }); it('should preserve safe alphanumeric characters and spaces', async () => { const safeQuery = 'Test 123 Query ABC xyz'; await channel.search(safeQuery, 10); const calls = mockSession.run.mock.calls; const fulltextCalls = calls.filter((call: any[]) => call[0].includes('db.index.fulltext.queryNodes') ); fulltextCalls.forEach((call: any[]) => { const query = call[1].query; // Alphanumeric and spaces should remain unchanged expect(query).toBe('Test 123 Query ABC xyz'); }); }); it('should handle email addresses safely', async () => { const email = 'user@example.com'; await channel.search(email, 10); const calls = mockSession.run.mock.calls; const fulltextCalls = calls.filter((call: any[]) => call[0].includes('db.index.fulltext.queryNodes') ); fulltextCalls.forEach((call: any[]) => { const query = call[1].query; // @ and . should be escaped expect(query).toBe('user\\@example\\.com'); }); }); it('should escape dangerous BASE85 chars that could affect Cypher', async () => { const dangerousInputs = [ 'test$param', // $ - parameter injection risk 'test;DROP', // ; - statement separator 'test`template', // ` - backtick risk 'test=value', // = - assignment operator 'test#comment', // # - comment character ]; for (const input of dangerousInputs) { mockSession.run.mockClear(); await channel.search(input, 10); const calls = mockSession.run.mock.calls; const fulltextCalls = calls.filter((call: any[]) => call[0].includes('db.index.fulltext.queryNodes') ); fulltextCalls.forEach((call: any[]) => { const query = call[1].query; // All special chars should be escaped if (input.includes('$')) expect(query).toContain('\\$'); if (input.includes(';')) expect(query).toContain('\\;'); if (input.includes('`')) expect(query).toContain('\\`'); if (input.includes('=')) expect(query).toContain('\\='); if (input.includes('#')) expect(query).toContain('\\#'); }); } }); }); describe('Memory type filtering security', () => { it('should use parameterized queries for memory types', async () => { const memoryTypes = ['note', 'project']; await channel.search('test', 10, memoryTypes); // Verify all queries use parameters properly const calls = mockSession.run.mock.calls; calls.forEach((call: any[]) => { const query = call[0]; const params = call[1]; if (query.includes('memoryType')) { // Should use parameter placeholder expect(query).toMatch(/\$memoryTypes/); expect(params.memoryTypes).toEqual(memoryTypes); // Should NOT contain direct string values expect(query).not.toContain("'note'"); expect(query).not.toContain("'project'"); expect(query).not.toContain('"note"'); expect(query).not.toContain('"project"'); } }); }); it('should not allow SQL/Cypher injection through memory types', async () => { const maliciousTypes = [ "'; DROP DATABASE neo4j; //", "' OR 1=1 --", "') OR true; MATCH (n) DELETE n; //", ]; await channel.search('test', 10, maliciousTypes); const calls = mockSession.run.mock.calls; // The malicious strings should only appear in parameters, never in query calls.forEach((call: any[]) => { const query = call[0]; const params = call[1]; // Query should not contain any of the malicious strings maliciousTypes.forEach(malicious => { expect(query).not.toContain(malicious); expect(query).not.toContain('DROP'); expect(query).not.toContain('DELETE'); }); // But params should contain them (safely parameterized) if (params.memoryTypes) { expect(params.memoryTypes).toEqual(maliciousTypes); } }); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sylweriusz/mcp-neo4j-memory-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server