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