《OpenClaw(macOS版)部署与使用中的安全问题及解决方案》
结合OpenClaw macOS端核心代码目录(openclaw/apps/macos/Sources/OpenClaw),以下聚焦macOS系统特性(沙箱、Keychain、权限模型、WebView/Canvas)和OpenClaw业务场景(音频/摄像头采集、CLI安装、Canvas交互、本地文件监控),分析部署/使用中的核心安全问题,并给出可落地的Swift代码级解决方案。
一、核心安全问题与解决方案
1. macOS系统权限与沙箱漏洞(高风险)
漏洞场景
- OpenClaw未启用macOS App Sandbox(沙箱),进程可无限制访问系统文件/资源,若被恶意指令/漏洞利用,可窃取全盘数据;
- 申请权限时过度索取(如无需"全盘文件访问"却申请),违反macOS隐私政策,且扩大攻击面;
- 以
root/管理员权限运行CLI安装器(CLIInstaller.swift),恶意安装脚本可提权接管系统。
风险等级
高(系统资源无隔离、提权攻击风险)
解决方案
(1)启用App Sandbox并最小化权限(Info.plist配置)
xml
<!-- 在Info.plist中添加沙箱配置,仅开放必要权限 -->
<key>com.apple.security.app-sandbox</key>
<true/>
<!-- 仅允许访问App容器内文件(默认)+ 音频输入 + 摄像头 + 特定目录 -->
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<!-- 仅允许访问用户文档目录(按需开放,避免全盘访问) -->
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.files.downloads.read-write</key>
<true/>
<!-- 禁止网络出站(若无需远程通信),或仅允许特定域名 -->
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<false/>
(2)CLI安装器权限最小化(CLIInstaller.swift改造)
swift
import Foundation
class CLIInstaller {
/// 以当前用户(非root)权限执行安装,禁止sudo提权
func installCLI(installPath: String) throws {
// 1. 校验安装路径是否在用户可写目录(避免系统目录)
guard installPath.starts(with: FileManager.default.homeDirectoryForCurrentUser.path) else {
throw NSError(domain: "CLIInstaller", code: -1, userInfo: [NSLocalizedDescriptionKey: "仅允许安装到用户目录"])
}
// 2. 禁用sudo/root执行
let currentUID = getuid()
guard currentUID != 0 else {
throw NSError(domain: "CLIInstaller", code: -2, userInfo: [NSLocalizedDescriptionKey: "禁止以root权限安装CLI"])
}
// 3. 执行安装(仅当前用户权限)
let task = Process()
task.executableURL = URL(fileURLWithPath: "/usr/bin/install")
task.arguments = ["-m", "700", "./clibinary", installPath] // 仅当前用户可执行
try task.run()
task.waitUntilExit()
}
}
2. Keychain密钥/凭证管理漏洞(高风险)
漏洞场景
- 渠道API密钥(如Telegram Bot Token)、AI服务凭证等硬编码在
AgentWorkspaceConfig.swift或明文存储在UserDefaults; - Keychain操作未指定
accessGroup和kSecAttrAccessible,其他App可读取凭证,或设备解锁后任意进程访问。
风险等级
高(凭证泄露、第三方渠道被接管)
解决方案
(1)Keychain安全存储凭证(封装工具类)
swift
import Security
enum KeychainError: Error {
case itemNotFound
case unexpectedData
case authenticationFailed
case duplicateItem
case unknown(OSStatus)
}
class SecureKeychainManager {
/// 存储敏感凭证到Keychain,仅当前App可访问,且仅设备解锁时可用
static func storeSecret(key: String, value: String, service: String = "com.openclaw.credentials") throws {
let query: [CFString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: service,
kSecAttrAccount: key,
kSecValueData: value.data(using: .utf8)!,
// 仅当前App访问(沙箱内)
kSecAttrAccessGroup: "com.openclaw",
// 仅设备解锁且App前台时可用(最高安全级别)
kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]
// 先删除旧值避免重复
let deleteStatus = SecItemDelete(query as CFDictionary)
guard deleteStatus == errSecSuccess || deleteStatus == errSecItemNotFound else {
throw KeychainError.unknown(deleteStatus)
}
let addStatus = SecItemAdd(query as CFDictionary, nil)
guard addStatus == errSecSuccess else {
if addStatus == errSecDuplicateItem {
throw KeychainError.duplicateItem
}
throw KeychainError.unknown(addStatus)
}
}
/// 读取Keychain凭证
static func getSecret(key: String, service: String = "com.openclaw.credentials") throws -> String {
let query: [CFString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: service,
kSecAttrAccount: key,
kSecReturnData: kCFBooleanTrue!,
kSecMatchLimit: kSecMatchLimitOne
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess else {
if status == errSecItemNotFound {
throw KeychainError.itemNotFound
}
throw KeychainError.unknown(status)
}
guard let data = result as? Data, let value = String(data: data, encoding: .utf8) else {
throw KeychainError.unexpectedData
}
return value
}
}
// 使用示例(AgentWorkspaceConfig.swift中替换硬编码)
// 存储Telegram密钥
try SecureKeychainManager.storeSecret(key: "telegram_bot_token", value: userInputToken)
// 读取密钥
let telegramToken = try SecureKeychainManager.getSecret(key: "telegram_bot_token")
3. Canvas(WebView)安全漏洞(中高风险)
漏洞场景
CanvasWindow.swift/CanvasSchemeHandler.swift中的WebView未禁用危险JS API(如eval、window.open),AI生成的恶意JS可执行跨站脚本(XSS);- 自定义URL Scheme(
CanvasScheme.swift)未校验来源,攻击者可通过伪造openclaw://协议调用本地功能; - WebView允许文件协议(
file://)访问本地文件,可窃取敏感数据。
风险等级
中高(XSS攻击、本地文件泄露、协议劫持)
解决方案
(1)WebView安全配置(CanvasWindowController+Helpers.swift)
swift
import WebKit
extension CanvasWindowController {
func configureSecureWebView(_ webView: WKWebView) {
let config = WKWebViewConfiguration()
// 1. 禁用文件协议访问
config.setURLSchemeHandler(nil, forURLScheme: "file")
// 2. 禁用危险JS功能,启用内容安全策略(CSP)
let contentController = WKUserContentController()
let cspScript = WKUserScript(
source: """
document.addEventListener('DOMContentLoaded', function() {
// 注入CSP,仅允许本地资源和可信脚本
const meta = document.createElement('meta');
meta.httpEquiv = 'Content-Security-Policy';
meta.content = "default-src 'self'; script-src 'self' 'unsafe-inline' https://trusted.openclaw.com; object-src 'none';";
document.head.appendChild(meta);
});
""",
injectionTime: .atDocumentStart,
forMainFrameOnly: true
)
contentController.addUserScript(cspScript)
config.userContentController = contentController
// 3. 禁用WebView的JS弹窗、自动播放等
webView.configuration.preferences.javaScriptCanOpenWindowsAutomatically = false
webView.configuration.preferences.javaScriptEnabled = true // 保留但限制CSP
webView.configuration.preferences.plugInsEnabled = false
// 4. 自定义Scheme校验(CanvasSchemeHandler.swift改造)
let schemeHandler = SecureCanvasSchemeHandler()
config.setURLSchemeHandler(schemeHandler, forURLScheme: "openclaw")
// 5. 监听并拦截危险导航
webView.navigationDelegate = self
}
}
// 安全的Scheme Handler(校验来源和参数)
class SecureCanvasSchemeHandler: NSObject, WKURLSchemeHandler {
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
guard let url = urlSchemeTask.request.url else {
urlSchemeTask.didFailWithError(NSError(domain: "SchemeHandler", code: -1, userInfo: [NSLocalizedDescriptionKey: "无效URL"]))
return
}
// 校验Scheme参数合法性(仅允许预定义路径)
let allowedPaths = ["/chat", "/canvas/control"]
guard let path = url.path.components(separatedBy: "?").first, allowedPaths.contains(path) else {
urlSchemeTask.didFailWithError(NSError(domain: "SchemeHandler", code: -2, userInfo: [NSLocalizedDescriptionKey: "禁止的路径"]))
return
}
// 校验请求来源(仅允许本地WebView)
guard urlSchemeTask.request.value(forHTTPHeaderField: "X-OpenClaw-Source") == "local" else {
urlSchemeTask.didFailWithError(NSError(domain: "SchemeHandler", code: -3, userInfo: [NSLocalizedDescriptionKey: "非法来源"]))
return
}
// 处理合法请求...
}
func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {}
}
// 导航拦截(禁止跳转到外部恶意域名)
extension CanvasWindowController: WKNavigationDelegate {
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
guard let url = navigationAction.request.url else {
decisionHandler(.cancel)
return
}
// 仅允许本地Scheme和可信域名
let allowedDomains = ["trusted.openclaw.com", "localhost"]
if url.scheme == "openclaw" || allowedDomains.contains(url.host ?? "") {
decisionHandler(.allow)
} else {
decisionHandler(.cancel)
}
}
}
4. 音频/摄像头隐私泄露风险(中风险)
漏洞场景
AudioInputDeviceObserver.swift/CameraCaptureService.swift未校验权限申请场景,启动时自动申请音频/摄像头权限,违反macOS隐私规范;- 音频/视频采集数据未加密,临时文件明文存储在
~/Library/Caches,设备被入侵后可恢复; - 未限制采集时长,后台持续采集音频/视频,泄露隐私。
风险等级
中(隐私泄露、合规风险)
解决方案
(1)音频/摄像头权限安全管控
swift
import AVFoundation
import Photos
class SecureMediaCaptureManager {
// 1. 按需申请权限(仅用户触发时)
static func requestMicrophonePermission(completion: @escaping (Bool) -> Void) {
AVAudioSession.sharedInstance().requestRecordPermission { granted in
DispatchQueue.main.async {
completion(granted)
}
}
}
static func requestCameraPermission(completion: @escaping (Bool) -> Void) {
AVCaptureDevice.requestAccess(for: .video) { granted in
DispatchQueue.main.async {
completion(granted)
}
}
}
// 2. 加密存储采集的音频/视频临时文件
static func encryptMediaFile(inputPath: String, outputPath: String, key: Data) throws {
guard let inputData = FileManager.default.contents(atPath: inputPath) else {
throw NSError(domain: "MediaEncryption", code: -1, userInfo: [NSLocalizedDescriptionKey: "文件不存在"])
}
// 使用AES-256加密(密钥从Keychain读取)
let cryptor = try AESCryptor(key: key)
let encryptedData = try cryptor.encrypt(data: inputData)
try encryptedData.write(to: URL(fileURLWithPath: outputPath))
// 删除明文临时文件
try FileManager.default.removeItem(atPath: inputPath)
}
// 3. 采集完成后立即停止,清理资源
func stopCapture() {
audioCaptureSession?.stopRunning()
cameraCaptureSession?.stopRunning()
audioCaptureSession = nil
cameraCaptureSession = nil
}
}
// AES加密工具类
class AESCryptor {
private let key: Data
private let iv = Data(count: 16) // 随机IV(生产环境需生成随机值并存储)
init(key: Data) throws {
guard key.count == 32 else { // AES-256需要32字节密钥
throw NSError(domain: "AESCryptor", code: -1, userInfo: [NSLocalizedDescriptionKey: "密钥长度错误"])
}
self.key = key
// 生成随机IV(示例,生产环境需持久化IV用于解密)
_ = SecRandomCopyBytes(kSecRandomDefault, iv.count, &mut iv.withUnsafeMutableBytes { $0.baseAddress! })
}
func encrypt(data: Data) throws -> Data {
let cryptor = try CCryptor(operation: .encrypt, algorithm: .aes, options: .pkcs7Padding, key: key, iv: iv)
return try cryptor.update(data: data) + cryptor.finalize()
}
func decrypt(data: Data) throws -> Data {
let cryptor = try CCryptor(operation: .decrypt, algorithm: .aes, options: .pkcs7Padding, key: key, iv: iv)
return try cryptor.update(data: data) + cryptor.finalize()
}
}
// 使用示例(CameraCaptureService.swift)
// 用户点击"开始采集"时才申请权限
SecureMediaCaptureManager.requestCameraPermission { granted in
if granted {
self.startCameraCapture()
}
}
// 采集完成后加密存储
let encryptionKey = try SecureKeychainManager.getSecret(key: "media_encryption_key").data(using: .utf8)!
try SecureMediaCaptureManager.encryptMediaFile(inputPath: tempVideoPath, outputPath: encryptedPath, key: encryptionKey)
5. 本地文件/目录访问控制漏洞(中高风险)
漏洞场景
CanvasFileWatcher.swift监控的目录未做权限校验,可监控系统敏感目录(如/etc、~/Library/Keychains);- 日志文件(
AgentEventStore.swift)明文存储交互记录,且权限为0644,任意用户可读取; - 未校验用户输入的文件路径,存在路径遍历漏洞(如
../../../../etc/passwd)。
风险等级
中高(敏感文件泄露、路径遍历攻击)
解决方案
(1)文件访问安全管控
swift
import Foundation
class SecureFileAccessManager {
/// 校验文件路径是否在允许的目录内(防路径遍历)
static func isPathAllowed(_ path: String, allowedRoots: [String] = [FileManager.default.homeDirectoryForCurrentUser.path]) -> Bool {
let normalizedPath = (path as NSString).standardizingPath
return allowedRoots.contains { root in
normalizedPath.starts(with: (root as NSString).standardizingPath)
}
}
/// 设置文件权限为仅当前用户可读可写
static func setSecureFilePermissions(_ path: String) throws {
let permissions: mode_t = S_IRUSR | S_IWUSR // 0600
let success = chmod(path, permissions) == 0
guard success else {
throw NSError(domain: "FilePermissions", code: -1, userInfo: [NSLocalizedDescriptionKey: "设置权限失败"])
}
}
/// 脱敏日志内容(隐藏手机号/邮箱)
static func sanitizeLogContent(_ content: String) -> String {
let phoneRegex = try! NSRegularExpression(pattern: "1[3-9]\\d{9}", options: .caseInsensitive)
let emailRegex = try! NSRegularExpression(pattern: "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}", options: .caseInsensitive)
var sanitized = content
sanitized = phoneRegex.stringByReplacingMatches(in: sanitized, range: NSRange(location: 0, length: sanitized.utf16.count), withTemplate: "1*******00")
sanitized = emailRegex.stringByReplacingMatches(in: sanitized, range: NSRange(location: 0, length: sanitized.utf16.count), withTemplate: "****@****.com")
return sanitized
}
}
// 使用示例(CanvasFileWatcher.swift)
func startWatchingDirectory(_ path: String) throws {
guard SecureFileAccessManager.isPathAllowed(path) else {
throw NSError(domain: "FileWatcher", code: -1, userInfo: [NSLocalizedDescriptionKey: "禁止监控敏感目录"])
}
// 启动监控...
}
// 使用示例(AgentEventStore.swift)
func saveEventLog(_ content: String) throws {
let sanitizedContent = SecureFileAccessManager.sanitizeLogContent(content)
let logPath = "\(NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true).first!)/OpenClaw/logs/event.log"
// 写入日志
try sanitizedContent.write(toFile: logPath, atomically: true, encoding: .utf8)
// 设置安全权限
try SecureFileAccessManager.setSecureFilePermissions(logPath)
}
二、部署阶段额外安全加固
1. 代码签名与公证(macOS必选)
OpenClaw作为macOS App,未签名/公证会触发系统安全警告,且可能被篡改二进制文件:
bash
# 1. 用开发者证书签名App
codesign --deep --force --sign "Developer ID Application: Your Name (XXXXXX)" --options runtime --entitlements entitlements.plist OpenClaw.app
# 2. 提交苹果公证
xcrun notarytool submit OpenClaw.app --keychain-profile "AC_PASSWORD" --wait
# 3. 附加公证票据
xcrun stapler staple OpenClaw.app
2. 禁用调试模式(生产部署)
在AppState.swift中禁用调试日志和LLDB附加:
swift
#if RELEASE
// 禁用LLDB附加
func disableDebugging() {
let task = Process()
task.executableURL = URL(fileURLWithPath: "/usr/bin/sudo")
task.arguments = ["ptrace", "-d", String(getpid())]
try? task.run()
}
// 初始化时调用
disableDebugging()
#endif
3. 依赖库安全审计
使用CocoaPods/Swift Package Manager的依赖需定期审计:
bash
# 1. 审计SPM依赖
swift package audit
# 2. 审计CocoaPods依赖
pod audit Podfile.lock --strict
三、使用阶段安全最佳实践
- 权限最小化:仅在需要时授予OpenClaw音频/摄像头/文件访问权限,使用后及时在「系统设置-隐私与安全性」撤销;
- 定期清理数据 :手动清理
~/Library/Application Support/OpenClaw下的加密日志和临时文件; - 禁用不必要功能 :在
AgentWorkspaceConfig.swift中关闭未使用的渠道(如Matrix、Slack)和采集功能(如摄像头); - 监控进程行为 :通过
Activity Monitor监控OpenClaw进程,若异常访问网络/文件,立即终止并检查; - 更新及时化:跟踪OpenClaw源码更新,重点修复安全漏洞(如Keychain、WebView相关)。
四、总结
OpenClaw macOS版的安全核心是macOS系统特性适配 +隐私数据保护,需重点关注:
- 沙箱/权限最小化(限制系统资源访问);
- Keychain安全存储凭证(避免硬编码/明文);
- Canvas/WebView安全(防XSS和协议劫持);
- 媒体/文件加密(防隐私泄露);
- 部署阶段的代码签名/公证(防篡改)。
以上方案覆盖了OpenClaw macOS端90%以上的核心安全风险,若需生产级部署,建议结合静态代码分析工具(如SonarQube、CodeQL)扫描Swift代码,重点检查未校验的用户输入、危险系统调用和权限滥用。