diff --git a/__tests__/security.test.ts b/__tests__/security.test.ts index abb70fe..75ac843 100644 --- a/__tests__/security.test.ts +++ b/__tests__/security.test.ts @@ -307,6 +307,34 @@ describe('MCP Input Validation', () => { const result = await handler.execute('codegraph_search', { query: 'example', limit: -5 }); expect(result.isError).toBeFalsy(); }); + + // #230: getCodeGraph must reject a sensitive system directory passed as + // projectPath before opening it. The error surfaces through execute()'s + // catch as an isError result. /etc is sensitive on POSIX; C:\Windows on + // Windows (path.resolve is platform-specific, so each case is gated). + it.runIf(process.platform !== 'win32')( + 'rejects a sensitive POSIX projectPath (/etc) via the MCP handler', + async () => { + const result = await handler.execute('codegraph_search', { + query: 'example', + projectPath: '/etc', + }); + expect(result.isError).toBe(true); + expect(result.content[0].text).toMatch(/sensitive system directory/i); + } + ); + + it.runIf(process.platform === 'win32')( + 'rejects a sensitive Windows projectPath (C:\\Windows) via the MCP handler', + async () => { + const result = await handler.execute('codegraph_search', { + query: 'example', + projectPath: 'C:\\Windows', + }); + expect(result.isError).toBe(true); + expect(result.content[0].text).toMatch(/sensitive system directory/i); + } + ); }); describe('Atomic Writes', () => { diff --git a/src/mcp/tools.ts b/src/mcp/tools.ts index dfd4154..deb8dfd 100644 --- a/src/mcp/tools.ts +++ b/src/mcp/tools.ts @@ -15,7 +15,7 @@ import { readFileSync, writeSync, } from 'fs'; -import { clamp, validatePathWithinRoot } from '../utils'; +import { clamp, validatePathWithinRoot, validateProjectPath } from '../utils'; import { tmpdir } from 'os'; import { join } from 'path'; @@ -579,6 +579,18 @@ export class ToolHandler { return this.projectCache.get(projectPath)!; } + // Reject sensitive system directories before opening. Only validate a + // path that actually exists — a nested or not-yet-created sub-path of a + // real project must still be allowed to resolve UP to its .codegraph/ + // root below (issue #238), so we don't run the existence-checking + // validator on paths that are meant to walk up. + if (existsSync(projectPath)) { + const pathError = validateProjectPath(projectPath); + if (pathError) { + throw new Error(pathError); + } + } + // Walk up parent directories to find nearest .codegraph/ const resolvedRoot = findNearestCodeGraphRoot(projectPath);