]> dgit.raspbian.org Git - dovecot.git/commitdiff
[PATCH 2/2] lib-sieve: storage: file - Validate script stat path through dir_fd
authorTimo Sirainen <timo.sirainen@open-xchange.com>
Mon, 4 May 2026 13:10:13 +0000 (13:10 +0000)
committerNoah Meyerhans <noahm@debian.org>
Wed, 16 Sep 2026 19:06:35 +0000 (15:06 -0400)
Extend the symlink-escape protection added in the previous commit to the
stat performed by sieve_file_script_open(): an "include :personal" lookup
or any other indirect path that triggers sieve_file_script_stat() also
needs to refuse a symlink whose target leaves the personal storage
directory, otherwise the existence check succeeds and the file is opened
later via the safe path with an unhelpful "permission denied".

Add sieve_file_script_stat_safe(), which uses fstatat(AT_SYMLINK_NOFOLLOW)
to obtain the entry's own stat (lnk_st) and then, only if the entry is a
symlink, opens it through sieve_file_storage_open_safe() to validate the
target stays inside dir_fd and to fetch the resolved target's stat (st)
via fstat(). Non-symlink entries skip the open entirely.

Use the new helper in sieve_file_script_open() whenever fstorage->dir_fd
is available, falling back to the unsafe lstat+stat variant for
non-personal or single-file storages.

Gbp-Pq: Name 0002-lib-sieve-storage-file-Validate-script-stat-path-thr.patch

pigeonhole/src/lib-sieve/storage/file/sieve-file-script.c

index 52e752abe69612ae3a0ea8a57e3088b4e72f6e2e..0136c3acedcb405998bb8197a6c7621a25a79b22 100644 (file)
@@ -285,6 +285,57 @@ sieve_file_script_stat(const char *path, struct stat *st, struct stat *lnk_st)
        return 0;
 }
 
+/* TOCTOU-safe variant of sieve_file_script_stat() for directory storages.
+   Resolves the script entry beneath fstorage->dir_fd, refusing symlinks
+   whose target escapes the storage directory. *st gets the (resolved)
+   target's stat; *lnk_st gets the directory entry's own stat (i.e.
+   AT_SYMLINK_NOFOLLOW), matching the semantics of the unsafe variant. */
+static int
+sieve_file_script_stat_safe(struct sieve_file_storage *fstorage,
+                           const char *filename, struct stat *st,
+                           struct stat *lnk_st, const char **error_r)
+{
+       int fd, saved_errno;
+
+       i_assert(fstorage->dir_fd >= 0);
+
+       if (fstatat(fstorage->dir_fd, filename, lnk_st,
+                   AT_SYMLINK_NOFOLLOW) < 0) {
+               *error_r = t_strdup_printf(
+                       "fstatat(%s/%s) failed: %m",
+                       fstorage->path, filename);
+               return -1;
+       }
+
+       if (!S_ISLNK(lnk_st->st_mode)) {
+               *st = *lnk_st;
+               return 0;
+       }
+
+       /* Symlink: open it via the safe walker (which refuses any target
+          that leaves fstorage->dir_fd) and read st from the resulting fd.
+          The fd is only used to fetch the resolved stat and is closed
+          immediately. Open with O_NONBLOCK so that a target which resolves
+          to a FIFO or other special file inside the storage directory only
+          yields its stat instead of blocking the delivery process. */
+       if (sieve_file_storage_open_safe(fstorage, filename,
+                                        O_RDONLY | O_NONBLOCK,
+                                        &fd, error_r) < 0)
+               return -1;
+
+       if (fstat(fd, st) < 0) {
+               saved_errno = errno;
+               *error_r = t_strdup_printf(
+                       "fstat() failed for '%s/%s': %m",
+                       fstorage->path, filename);
+               i_close_fd(&fd);
+               errno = saved_errno;
+               return -1;
+       }
+       i_close_fd(&fd);
+       return 0;
+}
+
 static const char *
 path_split_filename(const char *path, const char **dir_path_r)
 {
@@ -353,7 +404,25 @@ static int sieve_file_script_open(struct sieve_script *script)
                                dir_path = path;
 
                                path = sieve_file_storage_path_extend(fstorage, filename);
-                               ret = sieve_file_script_stat(path, &st, &lnk_st);
+                               if (fstorage->dir_fd >= 0) {
+                                       const char *serror;
+
+                                       ret = sieve_file_script_stat_safe(
+                                               fstorage, filename, &st,
+                                               &lnk_st, &serror);
+                                       if (ret < 0 && errno == ELOOP) {
+                                               sieve_script_set_critical(
+                                                       script,
+                                                       "Failed to open sieve script: %s",
+                                                       serror);
+                                               script->storage->error_code =
+                                                       SIEVE_ERROR_NO_PERMISSION;
+                                               success = FALSE;
+                                       }
+                               } else {
+                                       ret = sieve_file_script_stat(
+                                               path, &st, &lnk_st);
+                               }
                        }
 
                } else {
@@ -451,8 +520,11 @@ sieve_file_script_get_stream(struct sieve_script *script,
 
        /* For directory-based storage, open the script via the storage
           directory fd so that path resolution refuses to follow symlinks
-          whose (recursive) target leaves the storage directory.
-          Single-file storages have no dir_fd, so fall back to plain open(). */
+          whose (recursive) target leaves the storage directory. The fd-based
+          walk also makes this TOCTOU-safe: even if intermediate path
+          components are mutated between the stat in sieve_file_script_open()
+          and this call, the resolution stays within the original directory.
+          Single-file storages have no dir_fd, so fall back to a plain open(). */
        if (fstorage->dir_fd >= 0 && fscript->filename != NULL &&
            *fscript->filename != '\0') {
                if (sieve_file_storage_open_safe(fstorage, fscript->filename,