diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 4d96345..1903f1d 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-02 - Insecure File Permissions Update +**Vulnerability:** Found insecure permission modification that could be redirected. +**Learning:** Permission modification APIs that use string paths can be redirected via symlinks. +**Prevention:** Always use POSIX open() with O_NOFOLLOW and apply permissions with fchmod(). diff --git a/Sources/Cacheout/Headless/DaemonMode.swift b/Sources/Cacheout/Headless/DaemonMode.swift index 23b86f7..eca257e 100644 --- a/Sources/Cacheout/Headless/DaemonMode.swift +++ b/Sources/Cacheout/Headless/DaemonMode.swift @@ -295,10 +295,22 @@ public actor DaemonMode: StatusSocket.DataSource { withIntermediateDirectories: true, attributes: [.posixPermissions: 0o700] ) - try FileManager.default.setAttributes( - [.posixPermissions: 0o700], - ofItemAtPath: config.stateDir.path - ) + // SECURITY: Use open with O_NOFOLLOW and fchmod to prevent TOCTOU symlink attacks + // when setting permissions on the state directory. + struct SecureOperationError: Error {} + 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) + } + if dirFd >= 0 { + if fchmod(dirFd, 0o700) != 0 { + close(dirFd) + throw SecureOperationError() + } + close(dirFd) + } else { + throw SecureOperationError() + } } catch { logger.error("Failed to create/secure state directory: \(error.localizedDescription, privacy: .public)") Foundation.exit(1)