]> dgit.raspbian.org Git - dovecot.git/commit
[PATCH 1/2] lib-sieve: storage: file - Refuse symlinks escaping personal storage...
authorTimo Sirainen <timo.sirainen@open-xchange.com>
Mon, 4 May 2026 13:09:37 +0000 (13:09 +0000)
committerNoah Meyerhans <noahm@debian.org>
Wed, 16 Sep 2026 19:06:35 +0000 (15:06 -0400)
commit858e97c48245c42fedb7cd6cc8b0b38934be37fb
treec1d25a545cfd2216e4e47d7d3679f395c62eaa68
parent555147d5366ebefddd6f9159621415597da62135
[PATCH 1/2] lib-sieve: storage: file - Refuse symlinks escaping personal storage directory

Pigeonhole's file storage followed any symlink encountered while resolving a
script path, including symlinks in personal (user-writable) storage whose
target lay outside the storage directory. In some non-recommended
configurations a user could exploit this through the include extension:
an "include :personal name;" lookup of ~/sieve/name.sieve transparently
followed a user-placed to e.g. another user's file readable by the mail
process, leaking its contents (or causing it to be parsed as Sieve).
Normally this shouldn't be possible, because sieve processes shouldn't
have any more privileges to read files than the local system user creating
the symlink.

Open the canonical (realpath'd) personal storage directory at storage init
time and keep an O_DIRECTORY|O_CLOEXEC fd to it. Resolve script content
reads through this fd by routing sieve_file_script_get_stream() via a new
sieve_file_storage_open_safe() wrapper around t_openat_safe(), which:

 - opens each path component with O_NOFOLLOW so symlinks are detected
   explicitly rather than transparently followed;
 - follows symlinks only when their (recursively resolved) target stays
   beneath dir_fd, refusing absolute targets and `..` past the storage
   root with ELOOP;
 - caps the symlink-hop count to bound resolution time.

Anchoring at dir_fd makes the lookup TOCTOU-safe even when intermediate
path components are mutated on disk concurrently: resolution stays
relative to the original directory inode and the safe walker still rejects
any target that leaves it.

Apply the protection only when storage->is_personal is set; admin-managed
global storage is trusted and may legitimately use cross-boundary symlinks.
Single-file storages (is_file=TRUE) keep dir_fd at -1 and fall back to the
existing open path. Also guard the dir_fd open with S_ISDIR() to handle the
autodetect quirk where storage_path can refer to a regular file even when
is_file is FALSE.

Gbp-Pq: Name 0001-lib-sieve-storage-file-Refuse-symlinks-escaping-pers.patch
pigeonhole/src/lib-sieve/storage/file/sieve-file-script.c
pigeonhole/src/lib-sieve/storage/file/sieve-file-storage.c
pigeonhole/src/lib-sieve/storage/file/sieve-file-storage.h