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
Expand Up @@ -31,3 +31,7 @@
**Vulnerability:** External shell command executed in `listLocalSnapshots()` triggered a deadlock when `tmutil` output exceeded 64KB, because stdout and stderr were read synchronously inside the process termination handler.
**Learning:** In Swift, reading from a process pipe synchronously inside a `terminationHandler` can result in a permanent deadlock if the child blocks writing to a full pipe, preventing it from exiting.
**Prevention:** Asynchronously drain pipes continuously while the process is running using background queues.
## 2024-05-23 - Fix TOCTOU vulnerability in file permission enforcement
**Vulnerability:** Enforcing file permissions using `chmod()` on a potentially user-controlled file path introduces a Time-of-Check to Time-of-Use (TOCTOU) symlink vulnerability.
**Learning:** `chmod()` operates on paths, allowing a malicious actor to swap the file for a symlink between when the path was verified and when the permissions were modified, potentially altering permissions of arbitrary files.
**Prevention:** Avoid `chmod()`. Safely obtain a file descriptor using `open()` with the `O_NOFOLLOW` flag to prevent following symlinks, and then apply permissions atomically using `fchmod()`.
13 changes: 11 additions & 2 deletions Sources/Cacheout/Headless/DaemonMode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -966,8 +966,17 @@ public actor DaemonMode: StatusSocket.DataSource {
return
}

// Enforce 0600 permissions
chmod(path, 0o600)
// Enforce 0600 permissions safely to avoid TOCTOU symlink vulnerabilities
URL(fileURLWithPath: path).withUnsafeFileSystemRepresentation { cPath in
guard let cPath = cPath else { return }
let fd = open(cPath, O_RDONLY | O_NOFOLLOW)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid blocking open on special files

Opening autopilot.json with open(..., O_RDONLY | O_NOFOLLOW) can block indefinitely if an attacker replaces the path with a FIFO (POSIX open on FIFO read end blocks until a writer appears). Because this runs in the config reload/startup path (loadConfig), the daemon can hang before reporting status, which is a new denial-of-service vector introduced by replacing non-blocking chmod(path, ...) with a blocking open call.

Useful? React with πŸ‘Β / πŸ‘Ž.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep permission fix path independent of read access

Using open(..., O_RDONLY | O_NOFOLLOW) as the prerequisite for fchmod means files that are owner-owned but currently unreadable (for example mode 000 or 0200) cannot be repaired to 0600 by this code path. The old implementation changed mode first and then read, but the new path logs a warning and then fails contents(atPath:), turning a recoverable mispermission into a persistent config load failure until manual intervention.

Useful? React with πŸ‘Β / πŸ‘Ž.

if fd >= 0 {
fchmod(fd, 0o600)
close(fd)
} else {
logger.warning("Failed to secure config file permissions (maybe a symlink?): errno \(errno)")
}
}

// Read file
guard let data = FileManager.default.contents(atPath: path) else {
Expand Down
Loading