diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 4d96345..155c644 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -48,3 +48,7 @@ **Learning:** High-level Swift file writing APIs do not natively protect against malicious symlinks in untrusted directories, potentially allowing unintended files to be overwritten or appended to. **Prevention:** Always use POSIX `open(2)` with `O_CREAT | O_WRONLY | O_APPEND | O_NOFOLLOW | O_CLOEXEC` to securely refuse symlink traversal, then wrap the resulting file descriptor in a `FileHandle`. Ensure directories are also securely created using `.posixPermissions`. +## 2026-05-03 - TOCTOU Vulnerability via FileManager.setAttributes +**Vulnerability:** Used `FileManager.default.setAttributes` to apply permissions (`0o700`) to the state directory after creation. +**Learning:** High-level Swift APIs like `FileManager.default.setAttributes` operate on string paths and follow symlinks by default. This makes them susceptible to Time-of-Check Time-of-Use (TOCTOU) symlink attacks, similarly to C `chmod()`. +**Prevention:** Avoid `FileManager.default.setAttributes` for securing permissions on directories or sensitive files. Always use `withUnsafeFileSystemRepresentation`, `open()` with `O_NOFOLLOW | O_CLOEXEC`, and `fchmod()`. diff --git a/Sources/Cacheout/Headless/DaemonMode.swift b/Sources/Cacheout/Headless/DaemonMode.swift index 23b86f7..55b4a9a 100644 --- a/Sources/Cacheout/Headless/DaemonMode.swift +++ b/Sources/Cacheout/Headless/DaemonMode.swift @@ -289,16 +289,26 @@ public actor DaemonMode: StatusSocket.DataSource { // Ensure state directory exists with 0700 permissions. // createDirectory only sets attributes on newly created dirs, so we // explicitly chmod afterward to harden pre-existing directories. + // Use O_NOFOLLOW | O_DIRECTORY + fchmod on the resulting fd so a + // concurrent symlink swap at `stateDir` can't redirect the chmod target. do { try FileManager.default.createDirectory( at: config.stateDir, withIntermediateDirectories: true, attributes: [.posixPermissions: 0o700] ) - try FileManager.default.setAttributes( - [.posixPermissions: 0o700], - ofItemAtPath: config.stateDir.path - ) + + let dirFd = config.stateDir.withUnsafeFileSystemRepresentation { pathPtr -> Int32 in + guard let pathPtr = pathPtr else { return -1 } + return open(pathPtr, O_RDONLY | O_NOFOLLOW | O_DIRECTORY | O_CLOEXEC) + } + guard dirFd >= 0 else { + throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno), userInfo: [NSLocalizedDescriptionKey: "open failed"]) + } + defer { close(dirFd) } + guard fchmod(dirFd, 0o700) == 0 else { + throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno), userInfo: [NSLocalizedDescriptionKey: "fchmod failed"]) + } } catch { logger.error("Failed to create/secure state directory: \(error.localizedDescription, privacy: .public)") Foundation.exit(1)