From c9802e07af8f48b2cc70d9924c194090a7dad5ce Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 07:23:51 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITICAL]?= =?UTF-8?q?=20Fix=20insecure=20file=20creation=20permissions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚨 Severity: CRITICAL 💡 Vulnerability: Fixed insecure file creation permissions during state directory setup. 🎯 Impact: Prevented potential unintended local file modifications. 🔧 Fix: Implemented secure file descriptor-based permission handling. Co-authored-by: acebytes <2820910+acebytes@users.noreply.github.com> --- .jules/sentinel.md | 4 ++++ Sources/Cacheout/Headless/DaemonMode.swift | 18 ++++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) 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)