diff --git a/.jules/sentinel.md b/.jules/sentinel.md index fd58571..456a5f2 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-05-21 - TOCTOU Symlink Vulnerability in Logging +**Vulnerability:** Found `FileHandle(forWritingTo:)` and `String.write(to:atomically:)` being used for writing to a log file, which follow symlinks by default. +**Learning:** High-level Swift file APIs follow symlinks by default, making them vulnerable to Time-of-Check to Time-of-Use (TOCTOU) symlink attacks if the directory is potentially untrusted. +**Prevention:** To securely create or append to files in potentially untrusted directories, use POSIX `open(2)` with flags such as `O_CREAT | O_WRONLY | O_APPEND | O_NOFOLLOW | O_CLOEXEC` to explicitly refuse symlinks, then wrap the resulting file descriptor in a `FileHandle`. diff --git a/Sources/Cacheout/Cleaner/CacheCleaner.swift b/Sources/Cacheout/Cleaner/CacheCleaner.swift index 2111e6a..1d372c6 100644 --- a/Sources/Cacheout/Cleaner/CacheCleaner.swift +++ b/Sources/Cacheout/Cleaner/CacheCleaner.swift @@ -34,6 +34,7 @@ import Foundation import AppKit +import Darwin actor CacheCleaner { private let fileManager = FileManager.default @@ -261,12 +262,19 @@ actor CacheCleaner { let size = ByteCountFormatter.sharedFile.string(fromByteCount: bytesFreed) let entry = "[\(ISO8601DateFormatter.shared.string(from: Date()))] Cleaned \(category): \(size)\n" - if let handle = try? FileHandle(forWritingTo: logFile) { - handle.seekToEndOfFile() + let fd = logFile.withUnsafeFileSystemRepresentation { pathPtr -> Int32 in + guard let pathPtr = pathPtr else { return -1 } + return open(pathPtr, O_CREAT | O_WRONLY | O_APPEND | O_NOFOLLOW | O_CLOEXEC, 0o600) + } + + if fd != -1 { + let handle = FileHandle(fileDescriptor: fd, closeOnDealloc: true) handle.write(entry.data(using: .utf8) ?? Data()) - handle.closeFile() - } else { - try? entry.write(to: logFile, atomically: true, encoding: .utf8) + if #available(macOS 10.15.4, *) { + try? handle.close() + } else { + handle.closeFile() + } } } }