fix: harden macOS bar launch descriptor (#1533)

Hardens the macOS bar launch.json descriptor against untrusted/foreign-owned files and constrains the decoded descriptor; blocks dashboard file writes to the launch descriptor.
This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2026-06-15 23:24:13 -04:00
1 parent 619723e9b7
commit 10aff10a67
3 files changed
+106 -3

No files matched your search

@@ -1,4 +1,9 @@
import Foundation
#if os(Linux)
import Glibc
#else
import Darwin
#endif
import CCSBarCore
/// Reads `~/.ccs/bar/launch.json` and spawns the CCS bar server detached,
@@ -36,10 +41,80 @@ struct BarServerLauncher: Sendable {
private func loadDescriptor() -> BarLaunchDescriptor? {
let path = BarLaunchDescriptor.defaultPath(home: home)
guard FileManager.default.fileExists(atPath: path),
let data = FileManager.default.contents(atPath: path)
guard isSafeDescriptorFile(path),
let data = FileManager.default.contents(atPath: path),
let descriptor = try? JSONDecoder().decode(BarLaunchDescriptor.self, from: data),
isSafeDescriptor(descriptor)
else { return nil }
return try? JSONDecoder().decode(BarLaunchDescriptor.self, from: data)
return descriptor
}
/// Treat launch.json as untrusted because it lives under a user-writable CCS
/// directory. Refuse symlinks, files not owned by the current user, and files
/// writable by group/other before decoding executable details.
private func isSafeDescriptorFile(_ path: String) -> Bool {
let fm = FileManager.default
guard fm.fileExists(atPath: path) else { return false }
guard let linkValues = try? URL(fileURLWithPath: path).resourceValues(forKeys: [.isSymbolicLinkKey]),
linkValues.isSymbolicLink != true,
let attrs = try? fm.attributesOfItem(atPath: path),
(attrs[.type] as? FileAttributeType) == .typeRegular,
let owner = attrs[.ownerAccountID] as? NSNumber,
owner.uint32Value == getuid(),
let permissions = attrs[.posixPermissions] as? NSNumber
else { return false }
// Disallow group/other write bits. User-writable is expected so CCS can refresh it.
return (permissions.uint16Value & 0o022) == 0
}
/// Validate the descriptor schema and constrain it to the expected CCS bar
/// server command shape: runtime absolute path + absolute entry point +
/// exactly "bar serve". This blocks shell descriptors such as
/// /bin/sh -c attacker-command while preserving the installed launch path.
private func isSafeDescriptor(_ descriptor: BarLaunchDescriptor) -> Bool {
guard descriptor.schema == 1, descriptor.args.count == 3 else { return false }
guard descriptor.args[1] == "bar", descriptor.args[2] == "serve" else { return false }
guard isAbsolutePath(descriptor.runtime), isAbsolutePath(descriptor.args[0]) else { return false }
guard descriptor.home == home else { return false }
if let ccsHome = descriptor.ccsHome, !ccsHome.isEmpty, !isAbsolutePath(ccsHome) {
return false
}
let runtimeName = URL(fileURLWithPath: descriptor.runtime).lastPathComponent.lowercased()
let allowedRuntimes: Set<String> = ["node", "nodejs", "bun"]
guard allowedRuntimes.contains(runtimeName) else { return false }
guard FileManager.default.isExecutableFile(atPath: descriptor.runtime) else { return false }
let entry = descriptor.args[0]
let entryName = URL(fileURLWithPath: entry).lastPathComponent.lowercased()
guard entryName == "ccs.js" || entryName == "ccs.ts" else { return false }
guard isSafeEntrypointFile(entry), !isUnderCcsDir(entry) else { return false }
return true
}
private func isSafeEntrypointFile(_ path: String) -> Bool {
guard let attrs = try? FileManager.default.attributesOfItem(atPath: path),
(attrs[.type] as? FileAttributeType) == .typeRegular,
let permissions = attrs[.posixPermissions] as? NSNumber
else { return false }
return (permissions.uint16Value & 0o022) == 0
}
private func isUnderCcsDir(_ path: String) -> Bool {
let ccsPath = URL(fileURLWithPath: home)
.appendingPathComponent(".ccs")
.standardizedFileURL
.path
let targetPath = URL(fileURLWithPath: path).standardizedFileURL.path
return targetPath == ccsPath || targetPath.hasPrefix(ccsPath + "/")
}
private func isAbsolutePath(_ path: String) -> Bool {
path.hasPrefix("/")
}
// MARK: - Spawn from descriptor
+12
View File
@@ -437,6 +437,18 @@ export function validateFilePath(filePath: string): {
if (pathSegments.includes('.git') || pathSegments.includes('node_modules')) {
return { valid: false, readonly: false, error: 'Access to this path is not allowed' };
}
// launch.json is an executable descriptor consumed by the native macOS bar.
// 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'
) {
return { valid: false, readonly: false, error: 'Access to this path is not allowed' };
}
return { valid: true, readonly: false };
}
@@ -42,6 +42,22 @@ describe('validateFilePath', () => {
expect(result.readonly).toBe(false);
});
test('rejects writes to 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);
expect(result.valid).toBe(true);
expect(result.readonly).toBe(false);
});
test('rejects sibling paths that only share ~/.ccs prefix', () => {
const bypassPath = path.join(tempDir, '.ccs-evil', 'config.yaml');
const result = validateFilePath(bypassPath);