diff --git a/syscall.c b/syscall.c index 355fd5ab3..0727b602f 100644 --- a/syscall.c +++ b/syscall.c @@ -429,6 +429,10 @@ static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, siz && ((strcmp(abspath, "/proc") == 0 && strcmp(comp, "self") == 0) || (strcmp(abspath, "/dev") == 0 && strcmp(comp, "fd") == 0)); if (!namespace_pin && lst.st_uid != 0 && lst.st_uid != trusted_uid) { + rprintf(FERROR, + "refusing to follow a symlink owned by an untrusted user; " + "use --insecure-links locally or \"insecure links = yes\" in a " + "daemon module ONLY if every path component is trusted\n"); saved_errno = ELOOP; goto out; } diff --git a/testsuite/symlink-race-dest_test.py b/testsuite/symlink-race-dest_test.py index 04df413ae..7b500dd8d 100644 --- a/testsuite/symlink-race-dest_test.py +++ b/testsuite/symlink-race-dest_test.py @@ -50,8 +50,20 @@ os.symlink(outside, dest / 'sub') # attacker-owned dest component os.lchown(dest / 'sub', ATT_UID, ATT_UID) -subprocess.run(rsync_argv('-a', f'{src}/sub/', f'{dest}/sub/'), - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) +proc = subprocess.run( + rsync_argv('-a', f'{src}/sub/', f'{dest}/sub/'), + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + text=True, +) + +if proc.returncode == 0: + test_fail("attacker-owned destination symlink was not rejected") +if "refusing to follow a symlink owned by an untrusted user" not in proc.stderr: + test_fail( + "untrusted destination symlink failure omitted the actionable " + f"diagnostic: {proc.stderr!r}" + ) escaped = sorted(p.name for p in outside.iterdir()) if escaped: