Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -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.
18 changes: 15 additions & 3 deletions Sources/Cacheout/Headless/StatusSocket.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}
Expand Down