Skip to content
Open
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: 3 additions & 3 deletions src/filesystem/__tests__/lib.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ describe('Lib Functions', () => {
it('rejects disallowed paths', async () => {
const testPath = process.platform === 'win32' ? 'C:\\Windows\\System32\\file.txt' : '/etc/passwd';
await expect(validatePath(testPath))
.rejects.toThrow('Access denied - path outside allowed directories');
.rejects.toThrow('[path_outside_allowed_roots] Access denied - path outside allowed directories');
});

it('handles non-existent files by checking parent directory', async () => {
Expand Down Expand Up @@ -200,9 +200,9 @@ describe('Lib Functions', () => {
mockFs.realpath
.mockRejectedValueOnce(enoentError1)
.mockRejectedValueOnce(enoentError2);

await expect(validatePath(newFilePath))
.rejects.toThrow('Parent directory does not exist');
.rejects.toThrow('[path_parent_missing] Parent directory does not exist');
});

it('resolves relative paths against allowed directories instead of process.cwd()', async () => {
Expand Down
4 changes: 3 additions & 1 deletion src/filesystem/__tests__/startup-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ describe('Startup Directory Validation', () => {

// Should exit with error
expect(result.exitCode).toBe(1);
expect(result.stderr).toContain('Error: None of the specified directories are accessible');
expect(result.stderr).toMatch(
/argv_no_accessible_directories|missing_roots|Error: None of the specified directories are accessible/
);
});

it('should warn when path is not a directory', async () => {
Expand Down
6 changes: 3 additions & 3 deletions src/filesystem/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ for (const dir of allowedDirectories) {

// Exit only if ALL paths are inaccessible (and some were specified)
if (accessibleDirectories.length === 0 && allowedDirectories.length > 0) {
console.error("Error: None of the specified directories are accessible");
console.error("Error [missing_roots]: None of the specified directories are accessible");
process.exit(1);
}

Expand Down Expand Up @@ -745,8 +745,8 @@ server.server.oninitialized = async () => {
} else {
if (allowedDirectories.length > 0) {
console.error("Client does not support MCP Roots, using allowed directories set from server args:", allowedDirectories);
}else{
throw new Error(`Server cannot operate: No allowed directories available. Server was started without command-line directories and client either does not support MCP roots protocol or provided empty roots. Please either: 1) Start server with directory arguments, or 2) Use a client that supports MCP roots protocol and provides valid root directories.`);
} else {
throw new Error(`[missing_roots] Server cannot operate: No allowed directories available. Server was started without command-line directories and client either does not support MCP roots protocol or provided empty roots. Please either: 1) Start server with directory arguments, or 2) Use a client that supports MCP roots protocol and provides valid root directories.`);
}
}
};
Expand Down
35 changes: 28 additions & 7 deletions src/filesystem/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import { isPathWithinAllowedDirectories } from './path-validation.js';
// Global allowed directories - set by the main module
let allowedDirectories: string[] = [];

function formatFilesystemValidationError(code: string, detail: string): Error {
return new Error(`[${code}] ${detail}`);
}

// Function to set allowed directories from the main module
export function setAllowedDirectories(directories: string[]): void {
allowedDirectories = [...directories];
Expand Down Expand Up @@ -98,16 +102,24 @@ function resolveRelativePathAgainstAllowedDirectories(relativePath: string): str
// Security & Validation Functions
export async function validatePath(requestedPath: string): Promise<string> {
const expandedPath = expandHome(requestedPath);
const absolute = path.isAbsolute(expandedPath)
? path.resolve(expandedPath)
: resolveRelativePathAgainstAllowedDirectories(expandedPath);
let absolute: string;
try {
absolute = path.isAbsolute(expandedPath)
? path.resolve(expandedPath)
: resolveRelativePathAgainstAllowedDirectories(expandedPath);
} catch {
throw formatFilesystemValidationError('path_invalid', `Invalid path: ${requestedPath}`);
}

const normalizedRequested = normalizePath(absolute);

// Security: Check if path is within allowed directories before any file operations
const isAllowed = isPathWithinAllowedDirectories(normalizedRequested, allowedDirectories);
if (!isAllowed) {
throw new Error(`Access denied - path outside allowed directories: ${absolute} not in ${allowedDirectories.join(', ')}`);
throw formatFilesystemValidationError(
'path_outside_allowed_roots',
`Access denied - path outside allowed directories: ${absolute} not in ${allowedDirectories.join(', ')}`
);
}

// Security: Handle symlinks by checking their real path to prevent symlink attacks
Expand All @@ -116,7 +128,10 @@ export async function validatePath(requestedPath: string): Promise<string> {
const realPath = await fs.realpath(absolute);
const normalizedReal = normalizePath(realPath);
if (!isPathWithinAllowedDirectories(normalizedReal, allowedDirectories)) {
throw new Error(`Access denied - symlink target outside allowed directories: ${realPath} not in ${allowedDirectories.join(', ')}`);
throw formatFilesystemValidationError(
'path_outside_allowed_roots',
`Access denied - symlink target outside allowed directories: ${realPath} not in ${allowedDirectories.join(', ')}`
);
}
return realPath;
} catch (error) {
Expand All @@ -128,13 +143,19 @@ export async function validatePath(requestedPath: string): Promise<string> {
const realParentPath = await fs.realpath(parentDir);
const normalizedParent = normalizePath(realParentPath);
if (!isPathWithinAllowedDirectories(normalizedParent, allowedDirectories)) {
throw new Error(`Access denied - parent directory outside allowed directories: ${realParentPath} not in ${allowedDirectories.join(', ')}`);
throw formatFilesystemValidationError(
'path_outside_allowed_roots',
`Access denied - parent directory outside allowed directories: ${realParentPath} not in ${allowedDirectories.join(', ')}`
);
}
return absolute;
} catch {
throw new Error(`Parent directory does not exist: ${parentDir}`);
throw formatFilesystemValidationError('path_parent_missing', `Parent directory does not exist: ${parentDir}`);
}
}
if ((error as NodeJS.ErrnoException).code === 'EACCES' || (error as NodeJS.ErrnoException).code === 'EPERM') {
throw formatFilesystemValidationError('path_permission_denied', `Permission denied: ${absolute}`);
}
throw error;
}
}
Expand Down
Loading