Skip to content

Commit 02ea482

Browse files
authored
fix: validate projectPath in MCP handler to block sensitive directories (colbymchenry#230)
Validate projectPath in getCodeGraph so MCP clients can't open a codegraph in a sensitive system directory. Guarded with existsSync so nested/not-yet-created sub-paths still resolve up to the default project (preserves issue colbymchenry#238). Adds MCP-handler rejection tests (POSIX + Windows-gated); validated on a real Windows 11 VM. Closes colbymchenry#230
1 parent 7d5dd4c commit 02ea482

2 files changed

Lines changed: 41 additions & 1 deletion

File tree

__tests__/security.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,34 @@ describe('MCP Input Validation', () => {
307307
const result = await handler.execute('codegraph_search', { query: 'example', limit: -5 });
308308
expect(result.isError).toBeFalsy();
309309
});
310+
311+
// #230: getCodeGraph must reject a sensitive system directory passed as
312+
// projectPath before opening it. The error surfaces through execute()'s
313+
// catch as an isError result. /etc is sensitive on POSIX; C:\Windows on
314+
// Windows (path.resolve is platform-specific, so each case is gated).
315+
it.runIf(process.platform !== 'win32')(
316+
'rejects a sensitive POSIX projectPath (/etc) via the MCP handler',
317+
async () => {
318+
const result = await handler.execute('codegraph_search', {
319+
query: 'example',
320+
projectPath: '/etc',
321+
});
322+
expect(result.isError).toBe(true);
323+
expect(result.content[0].text).toMatch(/sensitive system directory/i);
324+
}
325+
);
326+
327+
it.runIf(process.platform === 'win32')(
328+
'rejects a sensitive Windows projectPath (C:\\Windows) via the MCP handler',
329+
async () => {
330+
const result = await handler.execute('codegraph_search', {
331+
query: 'example',
332+
projectPath: 'C:\\Windows',
333+
});
334+
expect(result.isError).toBe(true);
335+
expect(result.content[0].text).toMatch(/sensitive system directory/i);
336+
}
337+
);
310338
});
311339

312340
describe('Atomic Writes', () => {

src/mcp/tools.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
readFileSync,
1616
writeSync,
1717
} from 'fs';
18-
import { clamp, validatePathWithinRoot } from '../utils';
18+
import { clamp, validatePathWithinRoot, validateProjectPath } from '../utils';
1919
import { tmpdir } from 'os';
2020
import { join } from 'path';
2121

@@ -579,6 +579,18 @@ export class ToolHandler {
579579
return this.projectCache.get(projectPath)!;
580580
}
581581

582+
// Reject sensitive system directories before opening. Only validate a
583+
// path that actually exists — a nested or not-yet-created sub-path of a
584+
// real project must still be allowed to resolve UP to its .codegraph/
585+
// root below (issue #238), so we don't run the existence-checking
586+
// validator on paths that are meant to walk up.
587+
if (existsSync(projectPath)) {
588+
const pathError = validateProjectPath(projectPath);
589+
if (pathError) {
590+
throw new Error(pathError);
591+
}
592+
}
593+
582594
// Walk up parent directories to find nearest .codegraph/
583595
const resolvedRoot = findNearestCodeGraphRoot(projectPath);
584596

0 commit comments

Comments
 (0)