Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -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.
9 changes: 9 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path>');
allowedWorkspaces.push(cwd);
}

const server = new CodeSearchMCPServer({ allowedWorkspaces });
await server.start();
}
Expand Down
6 changes: 4 additions & 2 deletions src/utils/workspace-path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
7 changes: 4 additions & 3 deletions tests/unit/error-handling.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
26 changes: 26 additions & 0 deletions tests/unit/workspace-path-security.test.ts
Original file line number Diff line number Diff line change
@@ -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/);
});
});
Loading