fix(macos): harden exec approvals socket path and permissions

This commit is contained in:
Peter Steinberger
2026-03-01 23:37:07 +00:00
parent 6c5633598e
commit 912ddba81e
4 changed files with 226 additions and 3 deletions

View File

@@ -226,6 +226,7 @@ enum ExecApprovalsStore {
private static let defaultAsk: ExecAsk = .onMiss
private static let defaultAskFallback: ExecSecurity = .deny
private static let defaultAutoAllowSkills = false
private static let secureStateDirPermissions = 0o700
static func fileURL() -> URL {
OpenClawPaths.stateDirURL.appendingPathComponent("exec-approvals.json")
@@ -332,6 +333,7 @@ enum ExecApprovalsStore {
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
let data = try encoder.encode(file)
let url = self.fileURL()
self.ensureSecureStateDirectory()
try FileManager().createDirectory(
at: url.deletingLastPathComponent(),
withIntermediateDirectories: true)
@@ -343,6 +345,7 @@ enum ExecApprovalsStore {
}
static func ensureFile() -> ExecApprovalsFile {
self.ensureSecureStateDirectory()
let url = self.fileURL()
let existed = FileManager().fileExists(atPath: url.path)
let loaded = self.loadFile()
@@ -524,6 +527,18 @@ enum ExecApprovalsStore {
self.saveFile(file)
}
private static func ensureSecureStateDirectory() {
let url = OpenClawPaths.stateDirURL
do {
try FileManager().createDirectory(at: url, withIntermediateDirectories: true)
try FileManager().setAttributes(
[.posixPermissions: self.secureStateDirPermissions],
ofItemAtPath: url.path)
} catch {
self.logger.warning("exec approvals state dir permission hardening failed: \(error.localizedDescription, privacy: .public)")
}
}
private static func generateToken() -> String {
var bytes = [UInt8](repeating: 0, count: 24)
let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)

View File

@@ -544,6 +544,106 @@ private enum ExecHostExecutor {
}
}
enum ExecApprovalsSocketPathKind: Equatable {
case missing
case directory
case socket
case symlink
case other
}
enum ExecApprovalsSocketPathGuardError: LocalizedError {
case lstatFailed(path: String, code: Int32)
case parentPathInvalid(path: String, kind: ExecApprovalsSocketPathKind)
case socketPathInvalid(path: String, kind: ExecApprovalsSocketPathKind)
case unlinkFailed(path: String, code: Int32)
case createParentDirectoryFailed(path: String, message: String)
case setParentDirectoryPermissionsFailed(path: String, message: String)
var errorDescription: String? {
switch self {
case let .lstatFailed(path, code):
"lstat failed for \(path) (errno \(code))"
case let .parentPathInvalid(path, kind):
"socket parent path invalid (\(kind)) at \(path)"
case let .socketPathInvalid(path, kind):
"socket path invalid (\(kind)) at \(path)"
case let .unlinkFailed(path, code):
"unlink failed for \(path) (errno \(code))"
case let .createParentDirectoryFailed(path, message):
"socket parent directory create failed at \(path): \(message)"
case let .setParentDirectoryPermissionsFailed(path, message):
"socket parent directory chmod failed at \(path): \(message)"
}
}
}
enum ExecApprovalsSocketPathGuard {
static let parentDirectoryPermissions = 0o700
static func pathKind(at path: String) throws -> ExecApprovalsSocketPathKind {
var status = stat()
let result = lstat(path, &status)
if result != 0 {
if errno == ENOENT {
return .missing
}
throw ExecApprovalsSocketPathGuardError.lstatFailed(path: path, code: errno)
}
let fileType = status.st_mode & mode_t(S_IFMT)
if fileType == mode_t(S_IFDIR) { return .directory }
if fileType == mode_t(S_IFSOCK) { return .socket }
if fileType == mode_t(S_IFLNK) { return .symlink }
return .other
}
static func hardenParentDirectory(for socketPath: String) throws {
let parentURL = URL(fileURLWithPath: socketPath).deletingLastPathComponent()
let parentPath = parentURL.path
switch try self.pathKind(at: parentPath) {
case .missing, .directory:
break
case let kind:
throw ExecApprovalsSocketPathGuardError.parentPathInvalid(path: parentPath, kind: kind)
}
do {
try FileManager().createDirectory(at: parentURL, withIntermediateDirectories: true)
} catch {
throw ExecApprovalsSocketPathGuardError.createParentDirectoryFailed(
path: parentPath,
message: error.localizedDescription)
}
do {
try FileManager().setAttributes(
[.posixPermissions: self.parentDirectoryPermissions],
ofItemAtPath: parentPath)
} catch {
throw ExecApprovalsSocketPathGuardError.setParentDirectoryPermissionsFailed(
path: parentPath,
message: error.localizedDescription)
}
}
static func removeExistingSocket(at socketPath: String) throws {
let kind = try self.pathKind(at: socketPath)
switch kind {
case .missing:
return
case .socket:
break
case .directory, .symlink, .other:
throw ExecApprovalsSocketPathGuardError.socketPathInvalid(path: socketPath, kind: kind)
}
if unlink(socketPath) != 0, errno != ENOENT {
throw ExecApprovalsSocketPathGuardError.unlinkFailed(path: socketPath, code: errno)
}
}
}
private final class ExecApprovalsSocketServer: @unchecked Sendable {
private let logger = Logger(subsystem: "ai.openclaw", category: "exec-approvals.socket")
private let socketPath: String
@@ -583,7 +683,11 @@ private final class ExecApprovalsSocketServer: @unchecked Sendable {
self.socketFD = -1
}
if !self.socketPath.isEmpty {
unlink(self.socketPath)
do {
try ExecApprovalsSocketPathGuard.removeExistingSocket(at: self.socketPath)
} catch {
self.logger.warning("exec approvals socket cleanup failed: \(error.localizedDescription, privacy: .public)")
}
}
}
@@ -618,7 +722,14 @@ private final class ExecApprovalsSocketServer: @unchecked Sendable {
self.logger.error("exec approvals socket create failed")
return -1
}
unlink(self.socketPath)
do {
try ExecApprovalsSocketPathGuard.hardenParentDirectory(for: self.socketPath)
try ExecApprovalsSocketPathGuard.removeExistingSocket(at: self.socketPath)
} catch {
self.logger.error("exec approvals socket path hardening failed: \(error.localizedDescription, privacy: .public)")
close(fd)
return -1
}
var addr = sockaddr_un()
addr.sun_family = sa_family_t(AF_UNIX)
let maxLen = MemoryLayout.size(ofValue: addr.sun_path)
@@ -645,12 +756,18 @@ private final class ExecApprovalsSocketServer: @unchecked Sendable {
close(fd)
return -1
}
if chmod(self.socketPath, 0o600) != 0 {
self.logger.error("exec approvals socket chmod failed")
close(fd)
try? ExecApprovalsSocketPathGuard.removeExistingSocket(at: self.socketPath)
return -1
}
if listen(fd, 16) != 0 {
self.logger.error("exec approvals socket listen failed")
close(fd)
try? ExecApprovalsSocketPathGuard.removeExistingSocket(at: self.socketPath)
return -1
}
chmod(self.socketPath, 0o600)
self.logger.info("exec approvals socket listening at \(self.socketPath, privacy: .public)")
return fd
}