rsync: confine the daemon files-from open to the module root

A daemon serving a writable, non-chrooted module reads a client-requested
--files-from=:LIST through open_no_attacker_symlinks(), which follows a
symlink owned by uid 0 or the euid.  The module-root confinement in that
resolver (abspath_excluded_by_module) only fires when operator_path_resolve
is set, and this open left it clear -- so a trusted-owned symlink whose
target escapes the module was followed.

An attacker can obtain such a symlink without owning it: a --backup-dir push
makes the daemon back up the old destination symlink with the daemon's own
(root) ownership, and a parent-swap race can leave that root-owned backup
symlink pointing outside the module.  A later --files-from=:backup/... then
reads out-of-module file content as the file list, bypassing the same-uid
ownership constraint that normally protects files-from.

Set operator_path_resolve around the files-from open so the ownership walk
also refuses a trusted-owned symlink that redirects the list outside the
module root.  A daemon has no rsyncd.conf "files from" of its own, so this
path is always client-requested and confining it is unconditional.  No-op off
a daemon (the module-root check only fires when am_daemon).

The same ownership-walk opener backs the daemon merge/--exclude-from reads in
exclude.c, but those also load the module's own "include from"/"exclude from"
admin files, which on a non-chrooted module may legitimately live outside the
module; confining them there needs a client-vs-admin distinction and is left
to a separate change.
This commit is contained in:
Andrew Tridgell committed 2026-07-23 08:28:36 +10:00
1 parent 3b826d6683
commit 3fe1ed512c
1 file changed
+11 -1
+11 -1
View File
@@ -27,6 +27,7 @@
extern int module_id;
extern int local_server;
extern int sanitize_paths;
extern int operator_path_resolve;
extern int trust_sender_args;
extern int trust_sender_filter;
extern unsigned int module_dirlen;
@@ -2550,8 +2551,17 @@ int parse_arguments(int *argc_p, const char ***argv_p)
}
/* Operator-supplied path that may transit attacker-writable
* parents; refuse symlinks not owned by uid 0 or our euid,
* as for --exclude-from/--include-from/--filter in exclude.c. */
* as for --exclude-from/--include-from/--filter in exclude.c.
* A daemon reads this list from a CLIENT-requested path
* (--files-from=:LIST) and it must stay inside the module:
* operator_path_resolve makes the ownership walk also refuse a
* (trusted-owned) symlink that redirects the list outside the
* module root -- e.g. a root-owned backup symlink. No-op off a
* daemon (the module-root check only fires when am_daemon). */
int save_opr = operator_path_resolve;
operator_path_resolve = 1;
filesfrom_fd = open_no_attacker_symlinks(files_from, O_RDONLY|O_BINARY, 0);
operator_path_resolve = save_opr;
if (filesfrom_fd < 0) {
snprintf(err_buf, sizeof err_buf,
"failed to open files-from file %s: %s\n",