diff --git a/receiver.c b/receiver.c index 6b4b369..8031b8f 100644 --- a/receiver.c +++ b/receiver.c @@ -66,6 +66,7 @@ extern char sender_file_sum[MAX_DIGEST_LEN]; extern struct file_list *cur_flist, *first_flist, *dir_flist; extern filter_rule_list daemon_filter_list; extern OFF_T preallocated_len; +extern int fuzzy_basis; extern struct name_num_item *xfer_sum_nni; extern int xfer_sum_len; @@ -551,6 +552,8 @@ int recv_files(int f_in, int f_out, char *local_name) progress_init(); while (1) { + const char *basedir = NULL; + cleanup_disable(); /* This call also sets cur_flist. */ @@ -716,28 +719,34 @@ int recv_files(int f_in, int f_out, char *local_name) fnamecmp = get_backup_name(fname); break; case FNAMECMP_FUZZY: + if (fuzzy_basis == 0) { + rprintf(FERROR_XFER, "rsync: refusing malicious fuzzy operation for %s\n", xname); + exit_cleanup(RERR_PROTOCOL); + } if (file->dirname) { - pathjoin(fnamecmpbuf, sizeof fnamecmpbuf, file->dirname, xname); - fnamecmp = fnamecmpbuf; - } else - fnamecmp = xname; + basedir = file->dirname; + } + fnamecmp = xname; break; default: if (fnamecmp_type > FNAMECMP_FUZZY && fnamecmp_type-FNAMECMP_FUZZY <= basis_dir_cnt) { fnamecmp_type -= FNAMECMP_FUZZY + 1; if (file->dirname) { - stringjoin(fnamecmpbuf, sizeof fnamecmpbuf, - basis_dir[fnamecmp_type], "/", file->dirname, "/", xname, NULL); - } else - pathjoin(fnamecmpbuf, sizeof fnamecmpbuf, basis_dir[fnamecmp_type], xname); + pathjoin(fnamecmpbuf, sizeof fnamecmpbuf, basis_dir[fnamecmp_type], file->dirname); + basedir = fnamecmpbuf; + } else { + basedir = basis_dir[fnamecmp_type]; + } + fnamecmp = xname; } else if (fnamecmp_type >= basis_dir_cnt) { rprintf(FERROR, "invalid basis_dir index: %d.\n", fnamecmp_type); exit_cleanup(RERR_PROTOCOL); - } else - pathjoin(fnamecmpbuf, sizeof fnamecmpbuf, basis_dir[fnamecmp_type], fname); - fnamecmp = fnamecmpbuf; + } else { + basedir = basis_dir[fnamecmp_type]; + fnamecmp = fname; + } break; } if (!fnamecmp || (daemon_filter_list.head @@ -760,7 +769,7 @@ int recv_files(int f_in, int f_out, char *local_name) } /* open the file */ - fd1 = do_open(fnamecmp, O_RDONLY, 0); + fd1 = secure_relative_open(basedir, fnamecmp, O_RDONLY, 0); if (fd1 == -1 && protocol_version < 29) { if (fnamecmp != fname) { @@ -771,14 +780,20 @@ int recv_files(int f_in, int f_out, char *local_name) if (fd1 == -1 && basis_dir[0]) { /* pre-29 allowed only one alternate basis */ - pathjoin(fnamecmpbuf, sizeof fnamecmpbuf, - basis_dir[0], fname); - fnamecmp = fnamecmpbuf; + basedir = basis_dir[0]; + fnamecmp = fname; fnamecmp_type = FNAMECMP_BASIS_DIR_LOW; - fd1 = do_open(fnamecmp, O_RDONLY, 0); + fd1 = secure_relative_open(basedir, fnamecmp, O_RDONLY, 0); } } + if (basedir) { + // for the following code we need the full + // path name as a single string + pathjoin(fnamecmpbuf, sizeof fnamecmpbuf, basedir, fnamecmp); + fnamecmp = fnamecmpbuf; + } + one_inplace = inplace_partial && fnamecmp_type == FNAMECMP_PARTIAL_DIR; updating_basis_or_equiv = one_inplace || (inplace && (fnamecmp == fname || fnamecmp_type == FNAMECMP_BACKUP)); diff --git a/syscall.c b/syscall.c index d92074a..47c5ea5 100644 --- a/syscall.c +++ b/syscall.c @@ -33,6 +33,8 @@ #include #endif +#include "ifuncs.h" + extern int dry_run; extern int am_root; extern int am_sender; @@ -712,3 +714,82 @@ int do_open_nofollow(const char *pathname, int flags) return fd; } + +/* + open a file relative to a base directory. The basedir can be NULL, + in which case the current working directory is used. The relpath + must be a relative path, and the relpath must not contain any + elements in the path which follow symlinks (ie. like O_NOFOLLOW, but + applies to all path components, not just the last component) + + The relpath must also not contain any ../ elements in the path +*/ +int secure_relative_open(const char *basedir, const char *relpath, int flags, mode_t mode) +{ + if (!relpath || relpath[0] == '/') { + // must be a relative path + errno = EINVAL; + return -1; + } + if (strncmp(relpath, "../", 3) == 0 || strstr(relpath, "/../")) { + // no ../ elements allowed in the relpath + errno = EINVAL; + return -1; + } + +#if !defined(O_NOFOLLOW) || !defined(O_DIRECTORY) + // really old system, all we can do is live with the risks + if (!basedir) { + return open(relpath, flags, mode); + } + char fullpath[MAXPATHLEN]; + pathjoin(fullpath, sizeof fullpath, basedir, relpath); + return open(fullpath, flags, mode); +#else + int dirfd = AT_FDCWD; + if (basedir != NULL) { + dirfd = openat(AT_FDCWD, basedir, O_RDONLY | O_DIRECTORY); + if (dirfd == -1) { + return -1; + } + } + int retfd = -1; + + char *path_copy = my_strdup(relpath, __FILE__, __LINE__); + if (!path_copy) { + return -1; + } + + for (const char *part = strtok(path_copy, "/"); + part != NULL; + part = strtok(NULL, "/")) + { + int next_fd = openat(dirfd, part, O_RDONLY | O_DIRECTORY | O_NOFOLLOW); + if (next_fd == -1 && errno == ENOTDIR) { + if (strtok(NULL, "/") != NULL) { + // this is not the last component of the path + errno = ELOOP; + goto cleanup; + } + // this could be the last component of the path, try as a file + retfd = openat(dirfd, part, flags | O_NOFOLLOW, mode); + goto cleanup; + } + if (next_fd == -1) { + goto cleanup; + } + if (dirfd != AT_FDCWD) close(dirfd); + dirfd = next_fd; + } + + // the path must be a directory + errno = EINVAL; + +cleanup: + free(path_copy); + if (dirfd != AT_FDCWD) { + close(dirfd); + } + return retfd; +#endif // O_NOFOLLOW, O_DIRECTORY +}