Skip to main content
Glama
security.test.ts12.5 kB
// RAG MCP Server Security Test - Design Doc: rag-mcp-server-design.md (v1.1) // Generated: 2025-10-31 // Test Type: Security Test (Minimal Essential Tests) // Implementation Timing: After core implementations complete // Note: Reduced from 43 to 10 tests based on YAGNI principle and avoiding redundancy import { mkdir, rm, symlink, writeFile } from 'node:fs/promises' import { resolve } from 'node:path' import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' import { DocumentParser, ValidationError } from '../../parser/index.js' import { RAGServer } from '../../server/index.js' // ============================================ // Test Configuration // ============================================ const testConfig = { dbPath: './tmp/test-security-db', modelName: 'Xenova/all-MiniLM-L6-v2', cacheDir: './tmp/models', baseDir: resolve('./'), // Project root (accessible to both tests/fixtures and tmp) maxFileSize: 100 * 1024 * 1024, // 100MB chunkSize: 512, chunkOverlap: 100, } // ============================================ // ============================================ describe('RAG MCP Server Security Test', () => { let server: RAGServer const fixturesDir = resolve('./tmp/test-security-fixtures') beforeAll(async () => { // Setup: Prepare environment for security testing await mkdir(testConfig.dbPath, { recursive: true }) await mkdir(fixturesDir, { recursive: true }) }) afterAll(async () => { // Cleanup: Delete security test data (only our directories) await rm(testConfig.dbPath, { recursive: true, force: true }) await rm(fixturesDir, { recursive: true, force: true }) }) beforeEach(async () => { // Initialize server before each test server = new RAGServer(testConfig) await server.initialize() // Create test fixture directory and files await mkdir(fixturesDir, { recursive: true }) await writeFile( resolve(fixturesDir, 'sample.txt'), 'This is a sample text file for security testing. TypeScript is great.' ) }) // -------------------------------------------- // S-001: No external network communication (except model download) // -------------------------------------------- describe('S-001: No external network communication (except model download)', () => { // AC interpretation: [Security requirement] No external communication detected by packet capture (except model download) // Validation: No external communication occurs during file ingestion → search workflow after server startup it('No external communication detected during file ingestion → search workflow after server startup (simulated)', async () => { // Network request monitoring (simulated) const networkRequests: string[] = [] const originalFetch = global.fetch // Mock fetch API (for external communication detection) global.fetch = vi.fn(async (url: RequestInfo | URL) => { networkRequests.push(url.toString()) return originalFetch(url) }) as typeof fetch // Ingest file const sampleFile = resolve(fixturesDir, 'sample.txt') await server.handleIngestFile({ filePath: sampleFile }) // Execute search await server.handleQueryDocuments({ query: 'TypeScript', limit: 5 }) // Verify no external communication occurred expect(networkRequests.length).toBe(0) // Restore fetch API global.fetch = originalFetch }) // AC interpretation: [Security requirement] Transformers.js model loaded from local cache // Validation: No network communication after initial model download on subsequent startups it('No network communication after initial model download on subsequent startups (simulated)', async () => { // Network request monitoring (simulated) const networkRequests: string[] = [] const originalFetch = global.fetch // Mock fetch API global.fetch = vi.fn(async (url: RequestInfo | URL) => { networkRequests.push(url.toString()) return originalFetch(url) }) as typeof fetch // Second initialization (model already cached) const server2 = new RAGServer(testConfig) await server2.initialize() // Verify no requests to HuggingFace API const huggingfaceRequests = networkRequests.filter((url) => url.includes('huggingface.co')) expect(huggingfaceRequests.length).toBe(0) // Restore fetch API global.fetch = originalFetch }) // AC interpretation: [Security requirement] LanceDB accesses local filesystem only // Validation: No network communication during LanceDB operations it('No network communication during LanceDB operations (initialization, insertion, search) (simulated)', async () => { // Network request monitoring (simulated) const networkRequests: string[] = [] const originalFetch = global.fetch // Mock fetch API global.fetch = vi.fn(async (url: RequestInfo | URL) => { networkRequests.push(url.toString()) return originalFetch(url) }) as typeof fetch // Execute LanceDB operations const sampleFile = resolve(fixturesDir, 'sample.txt') await server.handleIngestFile({ filePath: sampleFile }) await server.handleQueryDocuments({ query: 'TypeScript', limit: 5 }) // Verify no external communication occurred expect(networkRequests.length).toBe(0) // Restore fetch API global.fetch = originalFetch }) }) // -------------------------------------------- // S-002: Path traversal attack prevention // -------------------------------------------- describe('S-002: Path traversal attack prevention', () => { // AC interpretation: [Security requirement] Path traversal attacks (`../../etc/passwd`) are rejected // Validation: Calling ingest_file with invalid file path (e.g., `../../etc/passwd`) returns ValidationError it('Path traversal attack (e.g., ../../etc/passwd) rejected with ValidationError', async () => { const parser = new DocumentParser({ baseDir: fixturesDir, maxFileSize: 100 * 1024 * 1024, }) await expect(parser.parseFile('../../etc/passwd')).rejects.toThrow(ValidationError) }) // AC interpretation: [Security requirement] Access outside baseDir with absolute paths is rejected // Validation: Calling ingest_file with absolute path outside baseDir (e.g., `/etc/passwd`) returns ValidationError it('Absolute path outside baseDir (e.g., /etc/passwd) rejected with ValidationError', async () => { const parser = new DocumentParser({ baseDir: fixturesDir, maxFileSize: 100 * 1024 * 1024, }) await expect(parser.parseFile('/etc/passwd')).rejects.toThrow(ValidationError) }) // AC interpretation: [Security requirement] Access outside baseDir via symbolic links is rejected // Validation: Calling ingest_file with symbolic link pointing outside baseDir returns ValidationError it('Symbolic link pointing outside baseDir (e.g., link_to_etc_passwd) rejected with ValidationError', async () => { const parser = new DocumentParser({ baseDir: fixturesDir, maxFileSize: 100 * 1024 * 1024, }) // Create symbolic link (/etc/passwd → tmp/test-security-fixtures/link_to_etc_passwd) const linkPath = resolve(fixturesDir, 'link_to_etc_passwd') try { await symlink('/etc/passwd', linkPath) } catch (error) { // Ignore symlink creation failure (if already exists) } await expect(parser.parseFile(linkPath)).rejects.toThrow(ValidationError) // Cleanup await rm(linkPath, { force: true }) }) }) // -------------------------------------------- // S-003: No document content in logs // -------------------------------------------- describe('S-003: No document content in logs', () => { // AC interpretation: [Security requirement] Document content not output to logs // Validation: Logs during file ingestion do not contain document body it('Logs during file ingestion do not contain document body (max 100 characters allowed)', async () => { // Capture logs const logs: string[] = [] const originalLog = console.log console.log = (...args: unknown[]) => { logs.push(args.join(' ')) } // Create test file (containing confidential information) const testFile = resolve('./tmp/secret-document.txt') await writeFile( testFile, 'This is a secret document with confidential information: PASSWORD123' ) // Ingest file await server.handleIngestFile({ filePath: testFile }) // Verify document content not included in logs const containsSecret = logs.some( (log) => log.includes('PASSWORD123') || log.includes('confidential information') ) expect(containsSecret).toBe(false) // Restore console.log console.log = originalLog }) // AC interpretation: [Security requirement] Search queries not output to logs // Validation: Logs during search do not contain search query body (max 100 characters allowed) it('Logs during search do not contain search query body (max 100 characters allowed)', async () => { // Capture logs const logs: string[] = [] const originalLog = console.log console.log = (...args: unknown[]) => { logs.push(args.join(' ')) } // Ingest sample file const sampleFile = resolve(fixturesDir, 'sample.txt') await server.handleIngestFile({ filePath: sampleFile }) // Search with confidential query const secretQuery = 'secret query with confidential information PASSWORD123' await server.handleQueryDocuments({ query: secretQuery, limit: 5 }) // Verify search query not included in logs const containsQuery = logs.some( (log) => log.includes('PASSWORD123') || log.includes('confidential information') ) expect(containsQuery).toBe(false) // Restore console.log console.log = originalLog }) // AC interpretation: [Security requirement] Document content not included in error logs // Validation: Logs during parse errors do not contain document body it('Logs during parse errors do not contain document body', async () => { // Capture logs const logs: string[] = [] const originalError = console.error console.error = (...args: unknown[]) => { logs.push(args.join(' ')) } // Attempt to ingest non-existent file (error occurs) const nonExistentFile = resolve('./tmp/nonexistent-document.txt') try { await server.handleIngestFile({ filePath: nonExistentFile }) } catch (error) { // Error is expected } // Verify error logs do not contain confidential information (PASSWORD123, etc.) // Note: File path itself is allowed (max 100 characters) const containsPassword = logs.some((log) => log.includes('PASSWORD123')) expect(containsPassword).toBe(false) // Restore console.error console.error = originalError }) }) // -------------------------------------------- // S-004: MCP security best practices compliance // -------------------------------------------- describe('S-004: MCP security best practices compliance', () => { // AC interpretation: [Security requirement] Stack traces not included when errors occur in production environment // Validation: When NODE_ENV=production and error occurs, stack trace is not included it('Stack traces not included when errors occur in production environment (NODE_ENV=production)', async () => { // Set environment variable const originalEnv = process.env['NODE_ENV'] process.env['NODE_ENV'] = 'production' // Attempt to ingest non-existent file (error occurs) const nonExistentFile = resolve('./tmp/nonexistent.txt') try { await server.handleIngestFile({ filePath: nonExistentFile }) // Fail if error does not occur expect.fail('Expected error to be thrown') } catch (error) { const errorMessage = (error as Error).message // Verify stack trace is not included expect(errorMessage).not.toContain(' at ') expect(errorMessage).not.toContain('.ts:') expect(errorMessage).not.toContain('.js:') } // Restore environment variable process.env['NODE_ENV'] = originalEnv }) }) })

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/shinpr/mcp-local-rag'

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