fix: validate projectPath in MCP handler to block sensitive directories (#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 #238). Adds MCP-handler rejection tests (POSIX + Windows-gated); validated on a real Windows 11 VM. Closes #230
This commit is contained in:
@@ -307,6 +307,34 @@ describe('MCP Input Validation', () => {
|
|||||||
const result = await handler.execute('codegraph_search', { query: 'example', limit: -5 });
|
const result = await handler.execute('codegraph_search', { query: 'example', limit: -5 });
|
||||||
expect(result.isError).toBeFalsy();
|
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', () => {
|
describe('Atomic Writes', () => {
|
||||||
|
|||||||
+13
-1
@@ -15,7 +15,7 @@ import {
|
|||||||
readFileSync,
|
readFileSync,
|
||||||
writeSync,
|
writeSync,
|
||||||
} from 'fs';
|
} from 'fs';
|
||||||
import { clamp, validatePathWithinRoot } from '../utils';
|
import { clamp, validatePathWithinRoot, validateProjectPath } from '../utils';
|
||||||
import { tmpdir } from 'os';
|
import { tmpdir } from 'os';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
@@ -579,6 +579,18 @@ export class ToolHandler {
|
|||||||
return this.projectCache.get(projectPath)!;
|
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/
|
// Walk up parent directories to find nearest .codegraph/
|
||||||
const resolvedRoot = findNearestCodeGraphRoot(projectPath);
|
const resolvedRoot = findNearestCodeGraphRoot(projectPath);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user