diff --git a/.jules/sentinel.md b/.jules/sentinel.md index fd58571..9e8bd88 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -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. +## 2026-06-25 - Insecure Temporary File Creation (TOCTOU) +**Vulnerability:** A file write function used `Data.write(to:)` to create temporary files with default permissions before restricting them to `0o600` via `FileManager.default.setAttributes()`. +**Learning:** This creates a Time-of-Check to Time-of-Use (TOCTOU) race condition where an attacker could read the sensitive file contents in the brief window before permissions are tightened. +**Prevention:** Always use POSIX `open()` with `O_CREAT | O_WRONLY | O_EXCL | O_CLOEXEC` and the explicit mode `0o600` to atomically create the file with restricted permissions, then wrap the file descriptor in a `FileHandle` to write data. Remove any stale temporary file before using `O_EXCL` to avoid failure. diff --git a/Sources/CacheoutHelperLib/SysctlJournal.swift b/Sources/CacheoutHelperLib/SysctlJournal.swift index a14c902..c15cc10 100644 --- a/Sources/CacheoutHelperLib/SysctlJournal.swift +++ b/Sources/CacheoutHelperLib/SysctlJournal.swift @@ -307,14 +307,28 @@ public final class SysctlJournal { do { let data = try PropertyListEncoder().encode(state) - // Write to temp file (non-atomic — we control the rename ourselves). - try data.write(to: tmpURL) + // Remove any stale temp file first, otherwise O_EXCL will fail. + try? FileManager.default.removeItem(at: tmpURL) - // Set permissions to 0600 (root-only) on temp file before rename. - try FileManager.default.setAttributes( - [.posixPermissions: 0o600], - ofItemAtPath: tmpURL.path - ) + // Securely create temp file with 0600 permissions, preventing TOCTOU. + let fd = tmpURL.withUnsafeFileSystemRepresentation { pathPtr -> Int32 in + guard let pathPtr = pathPtr else { return -1 } + return open(pathPtr, O_CREAT | O_WRONLY | O_EXCL | O_CLOEXEC, 0o600) + } + + guard fd >= 0 else { + logger.error("open(2) failed to securely create temp file") + return false + } + + let handle = FileHandle(fileDescriptor: fd, closeOnDealloc: true) + if #available(macOS 10.15.4, *) { + try handle.write(contentsOf: data) + try handle.close() + } else { + handle.write(data) + handle.closeFile() + } // Atomic rename(2) — atomicity on APFS/HFS+. if rename(tmpURL.path, url.path) != 0 {