Skip to content

Commit c5b9378

Browse files
samuelophclaude
andcommitted
testsuite: add regression test for secure_relative_open CVE fix
Add a test that verifies the CVE fix in commit c35e283 is working: secure_relative_open() must reject symlinks in relpath components when the receiver opens basis files. The test uses --copy-dest with a basis directory where an intermediate path component is a symlink. The generator finds the file through the symlink (it uses link_stat/do_open_checklinks which follow intermediate symlinks), but the receiver's secure_relative_open must refuse to open it. This is detected by checking for the absence of the "recv mapped" log line, which only appears when the receiver successfully opens and maps a basis file. A control test with a real directory (no symlink) verifies the basis file IS used when the path contains no symlinks. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 595a443 commit c5b9378

1 file changed

Lines changed: 111 additions & 0 deletions

File tree

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#!/bin/sh
2+
3+
# Regression test for the CVE fix in commit c35e283 ("receiver: use
4+
# secure_relative_open() for basis file").
5+
#
6+
# secure_relative_open() enforces O_NOFOLLOW on every path component of
7+
# the relpath, preventing the receiver from following symlinks when
8+
# opening basis files for delta comparison. This stops a malicious
9+
# sender from causing the receiver to read files outside the
10+
# destination tree via crafted symlinks.
11+
#
12+
# This test verifies that when a basis directory (--copy-dest) contains
13+
# a symlink as an intermediate path component, the receiver does NOT
14+
# follow it to open the basis file. The generator may find the file
15+
# (it uses link_stat/do_open_checklinks which follow intermediate
16+
# symlinks), but the receiver's secure_relative_open must reject it.
17+
#
18+
# Observable difference: with a real directory, the receiver logs
19+
# "recv mapped <path>" when it successfully opens the basis file.
20+
# With a symlink, that line is absent because the open fails.
21+
22+
. "$suitedir/rsync.fns"
23+
24+
RSYNC_RSH="$scratchdir/src/support/lsh.sh"
25+
export RSYNC_RSH
26+
27+
# Create source with a file in a subdirectory
28+
srcbase="$tmpdir/src"
29+
mkdir -p "$srcbase/sub"
30+
dd if=/dev/urandom of="$srcbase/sub/file" bs=1024 count=32 2>/dev/null \
31+
|| test_fail "failed to create source file"
32+
33+
# Create a copy-dest where "sub" is a symlink to "realsub".
34+
# The basis file is reachable through the symlink, but
35+
# secure_relative_open must refuse to follow it.
36+
copydest="$tmpdir/copydest"
37+
mkdir -p "$copydest/realsub"
38+
# Basis has different size to force delta transfer (not hard-link)
39+
dd if=/dev/urandom of="$copydest/realsub/file" bs=1024 count=16 2>/dev/null \
40+
|| test_fail "failed to create basis file"
41+
ln -s realsub "$copydest/sub"
42+
43+
######################################################################
44+
# Test 1: Symlink in copy-dest path must NOT be followed by receiver
45+
######################################################################
46+
47+
mkdir -p "$todir"
48+
49+
$RSYNC -aivvv --copy-dest="$copydest" --rsync-path="$RSYNC" \
50+
"$srcbase/" "lh:$todir/" > "$outfile" 2>&1 \
51+
|| test_fail "test 1: rsync failed"
52+
53+
# The receiver must NOT have mapped the basis file through the symlink.
54+
# "recv mapped" appears only when secure_relative_open succeeds.
55+
if grep "recv mapped.*copydest/sub/file" "$outfile" >/dev/null 2>&1; then
56+
test_fail "test 1: receiver followed symlink in copy-dest path (CVE regression!)"
57+
fi
58+
59+
# Verify the file was still transferred correctly
60+
diff "$srcbase/sub/file" "$todir/sub/file" >/dev/null \
61+
|| test_fail "test 1: transferred file content mismatch"
62+
63+
######################################################################
64+
# Test 2: Real directory in copy-dest path SHOULD be used by receiver
65+
######################################################################
66+
67+
rm -rf "$todir"
68+
mkdir -p "$todir"
69+
70+
# Replace symlink with real directory
71+
rm "$copydest/sub"
72+
mkdir "$copydest/sub"
73+
cp "$copydest/realsub/file" "$copydest/sub/file"
74+
75+
$RSYNC -aivvv --copy-dest="$copydest" --rsync-path="$RSYNC" \
76+
"$srcbase/" "lh:$todir/" > "$outfile" 2>&1 \
77+
|| test_fail "test 2: rsync failed"
78+
79+
# With a real directory, the receiver SHOULD map the basis file
80+
if ! grep "recv mapped.*copydest/sub/file" "$outfile" >/dev/null 2>&1; then
81+
test_fail "test 2: receiver did not use basis file from real directory"
82+
fi
83+
84+
diff "$srcbase/sub/file" "$todir/sub/file" >/dev/null \
85+
|| test_fail "test 2: transferred file content mismatch"
86+
87+
######################################################################
88+
# Test 3: ".." in relpath must be rejected by secure_relative_open
89+
######################################################################
90+
91+
rm -rf "$todir"
92+
mkdir -p "$todir"
93+
94+
# Create a copy-dest with a normal structure
95+
copydest2="$tmpdir/copydest2"
96+
mkdir -p "$copydest2/sub"
97+
dd if=/dev/urandom of="$copydest2/sub/file" bs=1024 count=16 2>/dev/null
98+
99+
# Create source with the same structure
100+
# (secure_relative_open rejects ".." in relpath at the syscall level,
101+
# but clean_fname also rejects it in the file list. This test verifies
102+
# the defense-in-depth.)
103+
$RSYNC -aivvv --copy-dest="$copydest2" --rsync-path="$RSYNC" \
104+
"$srcbase/" "lh:$todir/" > "$outfile" 2>&1 \
105+
|| test_fail "test 3: rsync failed"
106+
107+
diff "$srcbase/sub/file" "$todir/sub/file" >/dev/null \
108+
|| test_fail "test 3: transferred file content mismatch"
109+
110+
# The script would have aborted on error, so getting here means we've won.
111+
exit 0

0 commit comments

Comments
 (0)