From 2514fdc29a3ea727ecbb3a4ed222c73102eadeea Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 07:35:52 +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 issue in StatusSocket.swift where `expandingTildeInPath` failed to prevent path traversal when validating configuration files. - Refactored `handleValidateConfig` to use `.standardizingPath` and strictly ensure paths resolve inside the allowed `~/.cacheout/` boundary. - Documented findings in .jules/sentinel.md. Co-authored-by: acebytes <2820910+acebytes@users.noreply.github.com> --- .jules/sentinel.md | 4 ++++ Sources/Cacheout/Headless/StatusSocket.swift | 18 +++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 .jules/sentinel.md diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 0000000..bd28a03 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2025-04-02 - Path Traversal in File Validation +**Vulnerability:** Path traversal vulnerability in `StatusSocket.swift`'s `validate_config` socket command. +**Learning:** `expandingTildeInPath` does not resolve `..` or sandbox the path, allowing a malicious user to access files outside the user's home directory. +**Prevention:** Use `.standardizingPath` after `.expandingTildeInPath`, and enforce directory boundaries by verifying the final path `.hasPrefix()` against a canonical allowed path, such as the user's home directory. diff --git a/Sources/Cacheout/Headless/StatusSocket.swift b/Sources/Cacheout/Headless/StatusSocket.swift index 12c516e..7a506af 100644 --- a/Sources/Cacheout/Headless/StatusSocket.swift +++ b/Sources/Cacheout/Headless/StatusSocket.swift @@ -474,14 +474,26 @@ public final class StatusSocket: @unchecked Sendable { } let expandedPath = (path as NSString).expandingTildeInPath + let standardizedPath = (expandedPath as NSString).standardizingPath + + let allowedPrefix = URL(fileURLWithPath: FileManager.default.homeDirectoryForCurrentUser.path) + .appendingPathComponent(".cacheout").path + "/" + + guard standardizedPath.hasPrefix(allowedPrefix) else { + sendSuccessResponse(fd: fd, data: [ + "valid": false, + "errors": ["Path traversal detected or path outside allowed directory: \(standardizedPath)"], + ] 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 +502,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 }