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 @@
## 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.
18 changes: 14 additions & 4 deletions Sources/Cacheout/Headless/StatusSocket.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}
Expand All @@ -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,
Expand Down