From 3f1dce30bc1a9743e3baab2cc479e32762d68cb5 Mon Sep 17 00:00:00 2001 From: "Kai (Tam Nhu) Tran" <61256810+kaitranntt@users.noreply.github.com> Date: Tue, 30 Jun 2026 12:33:46 -0400 Subject: [PATCH] fix: close bar launch case bypass (#1613) --- .../Sources/CCSBarApp/BarServerLauncher.swift | 17 ++++++++++++----- src/web-server/routes/route-helpers.ts | 13 ++++++++----- tests/unit/web-server/route-helpers.test.ts | 8 ++++++++ 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/macos-bar/Sources/CCSBarApp/BarServerLauncher.swift b/macos-bar/Sources/CCSBarApp/BarServerLauncher.swift index 7d5a7c64..b4e680d7 100644 --- a/macos-bar/Sources/CCSBarApp/BarServerLauncher.swift +++ b/macos-bar/Sources/CCSBarApp/BarServerLauncher.swift @@ -105,14 +105,21 @@ struct BarServerLauncher: Sendable { } private func isUnderCcsDir(_ path: String) -> Bool { - let ccsPath = URL(fileURLWithPath: home) - .appendingPathComponent(".ccs") - .standardizedFileURL - .path - let targetPath = URL(fileURLWithPath: path).standardizedFileURL.path + let ccsPath = pathForComparison( + URL(fileURLWithPath: home) + .appendingPathComponent(".ccs") + ) + let targetPath = pathForComparison(URL(fileURLWithPath: path)) return targetPath == ccsPath || targetPath.hasPrefix(ccsPath + "/") } + private func pathForComparison(_ url: URL) -> String { + url.standardizedFileURL + .resolvingSymlinksInPath() + .path + .lowercased() + } + private func isAbsolutePath(_ path: String) -> Bool { path.hasPrefix("/") } diff --git a/src/web-server/routes/route-helpers.ts b/src/web-server/routes/route-helpers.ts index e8758538..7e5ace2d 100644 --- a/src/web-server/routes/route-helpers.ts +++ b/src/web-server/routes/route-helpers.ts @@ -377,7 +377,9 @@ export function updateSettingsFile( */ function normalizePathForComparison(filePath: string): string { 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 { @@ -437,7 +439,8 @@ export function validateFilePath(filePath: string): { // Block access to sensitive subdirectories const relativePath = path.relative(ccsDir, normalizedPath); 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' }; } @@ -445,9 +448,9 @@ export function validateFilePath(filePath: string): { // It must only be written by trusted bar install/launch code paths, not the // generic dashboard file API. if ( - pathSegments.length === 2 && - pathSegments[0] === 'bar' && - pathSegments[1] === 'launch.json' + comparisonSegments.length === 2 && + comparisonSegments[0] === 'bar' && + comparisonSegments[1] === 'launch.json' ) { return { valid: false, readonly: false, error: 'Access to this path is not allowed' }; } diff --git a/tests/unit/web-server/route-helpers.test.ts b/tests/unit/web-server/route-helpers.test.ts index eaab554e..581d1ee9 100644 --- a/tests/unit/web-server/route-helpers.test.ts +++ b/tests/unit/web-server/route-helpers.test.ts @@ -50,6 +50,14 @@ describe('validateFilePath', () => { 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', () => { const filePath = path.join(tempDir, '.ccs', 'bar', 'serve.log'); const result = validateFilePath(filePath);