diff --git a/src/services/file-system-service.ts b/src/services/file-system-service.ts index 79cb1d17..f16361ec 100644 --- a/src/services/file-system-service.ts +++ b/src/services/file-system-service.ts @@ -23,7 +23,9 @@ export class FileSystemService { this.maxFileSize = maxFileSize; } if (allowedBasePaths) { - this.allowedBasePaths = allowedBasePaths.map(p => path.normalize(p)); + this.allowedBasePaths = allowedBasePaths + .filter(Boolean) + .map(basePath => path.normalize(path.resolve(basePath))); } } @@ -180,9 +182,9 @@ export class FileSystemService { // Check against allowed base paths if configured if (this.allowedBasePaths.length > 0) { const isAllowed = this.allowedBasePaths.some(basePath => - normalizedPath.startsWith(basePath) + this.isWithinAllowedBase(normalizedPath, basePath) ); - + if (!isAllowed) { this.logger.warn('Path outside allowed directories', { requestedPath, @@ -381,4 +383,16 @@ export class FileSystemService { return null; } } -} \ No newline at end of file + + private isWithinAllowedBase(targetPath: string, basePath: string): boolean { + const relative = path.relative(basePath, targetPath); + if (relative === '') { + return true; // Exact match + } + + const startsOutside = relative.startsWith('..'); + const isAbsolute = path.isAbsolute(relative); + + return !startsOutside && !isAbsolute; + } +} diff --git a/tests/unit/file-system-service.test.ts b/tests/unit/file-system-service.test.ts index 6b7a7d18..6e55d9aa 100644 --- a/tests/unit/file-system-service.test.ts +++ b/tests/unit/file-system-service.test.ts @@ -76,6 +76,16 @@ describe('FileSystemService', () => { new CCUIError('PATH_NOT_FOUND', 'Path not found: /home/user/documents', 404) ); }); + + it('should block paths that only share a prefix with allowed directories', async () => { + const baseDir = path.join(os.tmpdir(), 'ccui-allowed-base'); + const siblingDir = `${baseDir}-sibling`; // Same prefix, different directory + const restrictedService = new FileSystemService(undefined, [baseDir]); + + await expect(restrictedService.listDirectory(siblingDir)).rejects.toThrow( + new CCUIError('PATH_NOT_ALLOWED', 'Path is outside allowed directories', 403) + ); + }); }); describe('Recursive directory listing', () => { @@ -203,4 +213,4 @@ describe('FileSystemService', () => { expect(gitHead).toBe(null); }); }); -}); \ No newline at end of file +});