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
22 changes: 18 additions & 4 deletions src/services/file-system-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
}
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -381,4 +383,16 @@ export class FileSystemService {
return null;
}
}
}

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;
}
}
12 changes: 11 additions & 1 deletion tests/unit/file-system-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -203,4 +213,4 @@ describe('FileSystemService', () => {
expect(gitHead).toBe(null);
});
});
});
});