mirror of
https://github.com/RsyncProject/rsync.git
synced 2026-09-12 21:28:25 -04:00
daemon: treat exclude=/filter as a name filter, not a symlink boundary
The daemon exclude/filter chain is a name-based visibility/tamper filter, as in stock rsync (verified against 3.2.7): a symlink whose own name is not excluded is followed to an excluded target, and the documented symlink defense is `munge symlinks`, not the filter. Collapse ".." (via sanitize_path) before the daemon dest / temp-dir / backup-dir / partial-dir / basis filter checks so a "../excluded" path is matched by name like 3.2.7 (clean_fname's CFN_COLLAPSE_DOT_DOT_DIRS does not collapse "a/b/c/../../../secret"), and keep a leading "/" for a "path = /" module so an absolute filter rule still matches. The module-ROOT confinement of operator paths is unchanged (previous commit); only the in-module name match is restored to its 3.2.7 behaviour.
This commit is contained in:
1 parent
defd7110ac
commit
dd3e6bfece
2 files changed
+38
-19
No files matched your search
@@ -715,20 +715,24 @@ static char *get_local_name(struct file_list *flist, char *dest_path)
|
||||
dest_path = dot_dir_or_error();
|
||||
|
||||
if (daemon_filter_list.head) {
|
||||
char *slash = strrchr(dest_path, '/');
|
||||
/* Collapse ".." for the NAME-based daemon filter check so a "../excluded"
|
||||
* destination is matched by name, as stock rsync does on its sanitized
|
||||
* arg. Done on a copy: the daemon exclude/filter is name-based (a symlink
|
||||
* whose own name is not excluded is still followed -- see rsyncd.conf(5)
|
||||
* "munge symlinks"), and the real dest_path is left for the resolver. */
|
||||
char cleaned[MAXPATHLEN], *slash;
|
||||
if (!sanitize_path(cleaned, dest_path, NULL, 0, SP_KEEP_DOT_DIRS))
|
||||
strlcpy(cleaned, dest_path, sizeof cleaned);
|
||||
slash = strrchr(cleaned, '/');
|
||||
if (slash && (slash[1] == '\0' || (slash[1] == '.' && slash[2] == '\0')))
|
||||
*slash = '\0';
|
||||
else
|
||||
slash = NULL;
|
||||
if ((*dest_path != '.' || dest_path[1] != '\0')
|
||||
&& (check_filter(&daemon_filter_list, FLOG, dest_path, 0) < 0
|
||||
|| check_filter(&daemon_filter_list, FLOG, dest_path, 1) < 0)) {
|
||||
if ((*cleaned != '.' || cleaned[1] != '\0')
|
||||
&& (check_filter(&daemon_filter_list, FLOG, cleaned, 0) < 0
|
||||
|| check_filter(&daemon_filter_list, FLOG, cleaned, 1) < 0)) {
|
||||
rprintf(FERROR, "ERROR: daemon has excluded destination \"%s\"\n",
|
||||
dest_path);
|
||||
exit_cleanup(RERR_FILESELECT);
|
||||
}
|
||||
if (slash)
|
||||
*slash = '/';
|
||||
}
|
||||
|
||||
/* See what currently exists at the destination. */
|
||||
@@ -1237,15 +1241,25 @@ static void do_server_recv(int f_in, int f_out, int argc, char *argv[])
|
||||
char **dir_p;
|
||||
filter_rule_list *elp = &daemon_filter_list;
|
||||
|
||||
/* Collapse ".." and strip the module-dir prefix to get the module-relative
|
||||
* name, but keep a leading "/" for a "path = /" module (module_dirlen <= 1)
|
||||
* so an absolute (module-rooted) filter rule still matches. */
|
||||
char clean[MAXPATHLEN], *dir;
|
||||
for (dir_p = basis_dir; *dir_p; dir_p++) {
|
||||
char *dir = *dir_p;
|
||||
if (*dir == '/')
|
||||
dir += module_dirlen;
|
||||
if (!sanitize_path(clean, *dir_p, "/", 0, SP_DEFAULT))
|
||||
strlcpy(clean, *dir_p, sizeof clean);
|
||||
dir = clean + (*clean == '/' && module_dirlen > 1 ? module_dirlen : 0);
|
||||
if (check_filter(elp, FLOG, dir, 1) < 0)
|
||||
goto options_rejected;
|
||||
}
|
||||
if (partial_dir && *partial_dir == '/'
|
||||
&& check_filter(elp, FLOG, partial_dir + module_dirlen, 1) < 0) {
|
||||
if (partial_dir && *partial_dir == '/') {
|
||||
if (!sanitize_path(clean, partial_dir, "/", 0, SP_DEFAULT))
|
||||
strlcpy(clean, partial_dir, sizeof clean);
|
||||
dir = clean + (*clean == '/' && module_dirlen > 1 ? module_dirlen : 0);
|
||||
if (check_filter(elp, FLOG, dir, 1) < 0)
|
||||
goto options_rejected;
|
||||
}
|
||||
if (0) {
|
||||
options_rejected:
|
||||
rprintf(FERROR, "Your options have been rejected by the server.\n");
|
||||
exit_cleanup(RERR_SYNTAX);
|
||||
|
||||
@@ -2315,21 +2315,26 @@ int parse_arguments(int *argc_p, const char ***argv_p)
|
||||
}
|
||||
if (daemon_filter_list.head && !am_sender) {
|
||||
filter_rule_list *elp = &daemon_filter_list;
|
||||
/* Strip the module-dir prefix to get the module-relative name, but keep a
|
||||
* leading "/" for a "path = /" module (module_dirlen <= 1) so an absolute
|
||||
* (module-rooted) filter rule still matches. */
|
||||
if (tmpdir) {
|
||||
char *dir;
|
||||
char clean[MAXPATHLEN], *dir;
|
||||
if (!*tmpdir)
|
||||
goto options_rejected;
|
||||
dir = tmpdir + (*tmpdir == '/' ? module_dirlen : 0);
|
||||
clean_fname(dir, CFN_COLLAPSE_DOT_DOT_DIRS);
|
||||
if (!sanitize_path(clean, tmpdir, "/", 0, SP_DEFAULT))
|
||||
strlcpy(clean, tmpdir, sizeof clean);
|
||||
dir = clean + (*clean == '/' && module_dirlen > 1 ? module_dirlen : 0);
|
||||
if (check_filter(elp, FLOG, dir, 1) < 0)
|
||||
goto options_rejected;
|
||||
}
|
||||
if (backup_dir) {
|
||||
char *dir;
|
||||
char clean[MAXPATHLEN], *dir;
|
||||
if (!*backup_dir)
|
||||
goto options_rejected;
|
||||
dir = backup_dir + (*backup_dir == '/' ? module_dirlen : 0);
|
||||
clean_fname(dir, CFN_COLLAPSE_DOT_DOT_DIRS);
|
||||
if (!sanitize_path(clean, backup_dir, "/", 0, SP_DEFAULT))
|
||||
strlcpy(clean, backup_dir, sizeof clean);
|
||||
dir = clean + (*clean == '/' && module_dirlen > 1 ? module_dirlen : 0);
|
||||
if (check_filter(elp, FLOG, dir, 1) < 0)
|
||||
goto options_rejected;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user