From 9bccf61ef97c7b44a2dbeb6d5663651b12902244 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 07:34:44 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20Fix=20path?= =?UTF-8?q?=20traversal=20in=20config=20validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Resolves an arbitrary file read vulnerability in `StatusSocket.swift` where user-supplied paths were evaluated using `expandingTildeInPath` without boundary checks. - Sanitizes input using `URL.standardized.path` and restricts reads exclusively to the `~/.cacheout/` state directory prefix. Co-authored-by: acebytes <2820910+acebytes@users.noreply.github.com> --- .jules/sentinel.md | 4 ++++ Sources/Cacheout/Headless/StatusSocket.swift | 18 ++++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 .jules/sentinel.md diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 0000000..cbb6284 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2024-04-03 - Path Traversal in Config Validation +**Vulnerability:** Arbitrary file read in daemon UNIX socket via path traversal +**Learning:** `expandingTildeInPath` alone does not sandbox against path traversal sequences (like `../../etc/shadow`). +**Prevention:** Always use `.standardized` to resolve traversal sequences and validate boundaries using `.hasPrefix()` with a trailing slash against the allowed canonical directory. diff --git a/Sources/Cacheout/Headless/StatusSocket.swift b/Sources/Cacheout/Headless/StatusSocket.swift index 12c516e..f9884a6 100644 --- a/Sources/Cacheout/Headless/StatusSocket.swift +++ b/Sources/Cacheout/Headless/StatusSocket.swift @@ -474,14 +474,24 @@ public final class StatusSocket: @unchecked Sendable { } let expandedPath = (path as NSString).expandingTildeInPath + let standardizedPath = URL(fileURLWithPath: expandedPath).standardized.path + let allowedPrefix = NSHomeDirectory().appending("/.cacheout/") + + guard standardizedPath.hasPrefix(allowedPrefix) else { + sendSuccessResponse(fd: fd, data: [ + "valid": false, + "errors": ["Path traversal detected: file must be inside ~/.cacheout/"], + ] as [String: Any]) + return + } // lstat to reject symlinks to special files, FIFOs, devices, etc. var sb = Darwin.stat() - let statResult: Int32 = lstat(expandedPath, &sb) + let statResult: Int32 = lstat(standardizedPath, &sb) guard statResult == 0 else { sendSuccessResponse(fd: fd, data: [ "valid": false, - "errors": ["File not found: \(expandedPath)"], + "errors": ["File not found: \(standardizedPath)"], ] as [String: Any]) return } @@ -490,7 +500,7 @@ public final class StatusSocket: @unchecked Sendable { guard (sb.st_mode & S_IFMT) == S_IFREG else { sendSuccessResponse(fd: fd, data: [ "valid": false, - "errors": ["Not a regular file: \(expandedPath)"], + "errors": ["Not a regular file: \(standardizedPath)"], ] as [String: Any]) return } @@ -505,7 +515,7 @@ public final class StatusSocket: @unchecked Sendable { } do { - let fileData = try Data(contentsOf: URL(fileURLWithPath: expandedPath)) + let fileData = try Data(contentsOf: URL(fileURLWithPath: standardizedPath)) let errors = AutopilotConfigValidator.validate(data: fileData) sendSuccessResponse(fd: fd, data: [ "valid": errors.isEmpty,