fix: close bar launch case bypass (#1613)

This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2026-06-30 12:33:46 -04:00
1 parent 0e097f3301
commit 3f1dce30bc
3 files changed
+28 -10

No files matched your search

@@ -105,14 +105,21 @@ struct BarServerLauncher: Sendable {
} }
private func isUnderCcsDir(_ path: String) -> Bool { private func isUnderCcsDir(_ path: String) -> Bool {
let ccsPath = URL(fileURLWithPath: home) let ccsPath = pathForComparison(
.appendingPathComponent(".ccs") URL(fileURLWithPath: home)
.standardizedFileURL .appendingPathComponent(".ccs")
.path )
let targetPath = URL(fileURLWithPath: path).standardizedFileURL.path let targetPath = pathForComparison(URL(fileURLWithPath: path))
return targetPath == ccsPath || targetPath.hasPrefix(ccsPath + "/") return targetPath == ccsPath || targetPath.hasPrefix(ccsPath + "/")
} }
private func pathForComparison(_ url: URL) -> String {
url.standardizedFileURL
.resolvingSymlinksInPath()
.path
.lowercased()
}
private func isAbsolutePath(_ path: String) -> Bool { private func isAbsolutePath(_ path: String) -> Bool {
path.hasPrefix("/") path.hasPrefix("/")
} }
+8 -5
View File
@@ -377,7 +377,9 @@ export function updateSettingsFile(
*/ */
function normalizePathForComparison(filePath: string): string { function normalizePathForComparison(filePath: string): string {
const normalized = path.resolve(path.normalize(filePath)); const normalized = path.resolve(path.normalize(filePath));
return process.platform === 'win32' ? normalized.toLowerCase() : normalized; return process.platform === 'win32' || process.platform === 'darwin'
? normalized.toLowerCase()
: normalized;
} }
function isPathWithin(basePath: string, targetPath: string): boolean { function isPathWithin(basePath: string, targetPath: string): boolean {
@@ -437,7 +439,8 @@ export function validateFilePath(filePath: string): {
// Block access to sensitive subdirectories // Block access to sensitive subdirectories
const relativePath = path.relative(ccsDir, normalizedPath); const relativePath = path.relative(ccsDir, normalizedPath);
const pathSegments = relativePath.split(path.sep).filter(Boolean); const pathSegments = relativePath.split(path.sep).filter(Boolean);
if (pathSegments.includes('.git') || pathSegments.includes('node_modules')) { const comparisonSegments = pathSegments.map((segment) => segment.toLowerCase());
if (comparisonSegments.includes('.git') || comparisonSegments.includes('node_modules')) {
return { valid: false, readonly: false, error: 'Access to this path is not allowed' }; return { valid: false, readonly: false, error: 'Access to this path is not allowed' };
} }
@@ -445,9 +448,9 @@ export function validateFilePath(filePath: string): {
// It must only be written by trusted bar install/launch code paths, not the // It must only be written by trusted bar install/launch code paths, not the
// generic dashboard file API. // generic dashboard file API.
if ( if (
pathSegments.length === 2 && comparisonSegments.length === 2 &&
pathSegments[0] === 'bar' && comparisonSegments[0] === 'bar' &&
pathSegments[1] === 'launch.json' comparisonSegments[1] === 'launch.json'
) { ) {
return { valid: false, readonly: false, error: 'Access to this path is not allowed' }; return { valid: false, readonly: false, error: 'Access to this path is not allowed' };
} }
@@ -50,6 +50,14 @@ describe('validateFilePath', () => {
expect(result.readonly).toBe(false); expect(result.readonly).toBe(false);
}); });
test('rejects case variants of the macOS bar launch descriptor', () => {
const filePath = path.join(tempDir, '.ccs', 'BAR', 'LAUNCH.JSON');
const result = validateFilePath(filePath);
expect(result.valid).toBe(false);
expect(result.readonly).toBe(false);
});
test('still allows other writes inside the bar directory', () => { test('still allows other writes inside the bar directory', () => {
const filePath = path.join(tempDir, '.ccs', 'bar', 'serve.log'); const filePath = path.join(tempDir, '.ccs', 'bar', 'serve.log');
const result = validateFilePath(filePath); const result = validateFilePath(filePath);