diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 0000000..ed744cb --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,6 @@ +# Sentinel Journal + +## 2026-01-20 - Default File System Exposure +**Vulnerability:** The MCP server defaulted to allowing access to the entire file system (path traversal) when no `--allowed-workspace` arguments were provided. +**Learning:** "Fail open" defaults are dangerous, especially for tools exposed to LLMs which might explore the system. The developers likely intended this for ease of use but underestimated the risk. +**Prevention:** Always implement "fail closed" security. If configuration is missing, default to the most restrictive safe option (cwd) or deny access completely, rather than allowing everything. diff --git a/src/index.ts b/src/index.ts index 0b891cd..6c1c9b3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,6 +37,15 @@ function parseArgs(args: string[]): { allowedWorkspaces: string[] } { async function main() { const { allowedWorkspaces } = parseArgs(process.argv.slice(2)); + + // If no allowed workspaces are specified, default to the current working directory + if (allowedWorkspaces.length === 0) { + const cwd = process.cwd(); + console.error(`⚠️ No allowed workspaces specified. Defaulting to current working directory: ${cwd}`); + console.error('To allow other directories, use --allowed-workspace '); + allowedWorkspaces.push(cwd); + } + const server = new CodeSearchMCPServer({ allowedWorkspaces }); await server.start(); } diff --git a/src/utils/workspace-path.ts b/src/utils/workspace-path.ts index 968b636..f1ea6b0 100644 --- a/src/utils/workspace-path.ts +++ b/src/utils/workspace-path.ts @@ -53,9 +53,11 @@ export function validateAllowedPath( ): string { const normalized = path.resolve(requestedPath); - // If no allowed workspaces are configured, allow all paths + // If no allowed workspaces are configured, deny all access if (allowedWorkspaces.length === 0) { - return normalized; + throw new Error( + `Access denied: No allowed workspaces configured. Access to ${normalized} is denied.` + ); } const isAllowed = allowedWorkspaces.some(allowed => { diff --git a/tests/unit/error-handling.test.ts b/tests/unit/error-handling.test.ts index 8c7b4c2..194bf53 100644 --- a/tests/unit/error-handling.test.ts +++ b/tests/unit/error-handling.test.ts @@ -104,9 +104,10 @@ describe('Error Handling and Edge Cases', () => { ).toThrow(/Access denied/); }); - it('should allow any path when no workspaces are configured', () => { - const result = validateAllowedPath('/any/path', []); - expect(result).toBeDefined(); + it('should deny any path when no workspaces are configured', () => { + expect(() => + validateAllowedPath('/any/path', []) + ).toThrow(/Access denied/); }); it('should allow exact match of allowed workspace', () => { diff --git a/tests/unit/workspace-path-security.test.ts b/tests/unit/workspace-path-security.test.ts new file mode 100644 index 0000000..d9ac9a1 --- /dev/null +++ b/tests/unit/workspace-path-security.test.ts @@ -0,0 +1,26 @@ + +import { describe, it, expect } from '@jest/globals'; +import { validateAllowedPath } from '../../src/utils/workspace-path.js'; +import path from 'path'; + +describe('Workspace Path Security', () => { + it('should DENY access when allowedWorkspaces is empty', () => { + // Secure behavior: empty array means allow nothing + expect(() => { + validateAllowedPath('/etc/passwd', []); + }).toThrow(/Access denied/); + }); + + it('should ALLOW access to explicitly allowed workspace', () => { + const cwd = process.cwd(); + const result = validateAllowedPath(cwd, [cwd]); + expect(result).toBe(cwd); + }); + + it('should DENY access to path outside allowed workspace', () => { + const cwd = process.cwd(); + expect(() => { + validateAllowedPath('/etc/passwd', [cwd]); + }).toThrow(/Access denied/); + }); +});