Files
rsync/testsuite/operator-path-traversal-partial-dir-daemon_test.py
Andrew Tridgell ba3e9d681b testsuite: don't report a bogus escape when the scratch path has a space
The three operator-path-traversal daemon tests failed with "escaped: a '..'
traversal reached the excluded subtree" when the build path contained a space.
That reads like a confinement failure and is not one.

rsyncd.conf's "exclude" is a SPACE-SEPARATED list of patterns, so
"exclude = /ws test/.../secret/" is two patterns, neither of which is the
directory meant to be protected.  Nothing was excluded, so the traversal
reached a subtree that was never actually off limits.

Confirmed by running the same case with a "filter" rule, which the parser
deliberately does not split at an internal space: it passes, so the traversal
protection itself holds.

Left on "exclude" rather than switched to "filter" -- these tests exist to
cover the exclude path -- and skipped with the reason when the scratch path
makes that config inexpressible.

Worth knowing outside the testsuite: an operator whose module paths contain a
space gets no warning that "exclude" silently matched nothing.
2026-08-02 08:13:25 +10:00

68 lines
2.4 KiB
Python

#!/usr/bin/env python3
# Daemon --partial-dir ".." traversal into the module's excluded subtree, on a
# `path = /` module. (Reported by Omar Elsayed.) The peer-supplied --partial-dir
# climbs via ".." back into the excluded subtree; a staged partial must not land
# there. Sandbox-anchored under SCRATCHDIR (Omar's original wrote to the real
# /secret), so it runs unprivileged; the module still serves "/".
import os
import subprocess
from rsyncfns import (
SCRATCHDIR, rmtree, rsync_argv, start_test_daemon, test_fail, write_daemon_conf, test_skipped,
)
DAEMON_PORT = 12965
# rsyncd.conf's "exclude" is a SPACE-SEPARATED pattern list, so a path with a
# space in it cannot be expressed there at all -- the pattern silently matches
# nothing and the traversal below then "escapes" for a reason that has nothing
# to do with the protection under test. ("filter" is space-safe and does hold;
# verified by running this same case with a filter rule.) Skip rather than
# report a bogus escape.
if ' ' in str(SCRATCHDIR):
test_skipped("scratch path contains a space; rsyncd.conf 'exclude' is a "
"space-separated list and cannot express such a pattern")
SECRET = "PROTECTED-IN-EXCLUDED-SUBTREE\n"
base = SCRATCHDIR / 'traversal-partial-dir-daemon'
rmtree(base)
base.mkdir()
partial = base / 'partial-dir-daemon'
partial.mkdir()
secret = base / 'secret' # excluded subtree
secret.mkdir()
victim = secret / 'f0'
victim.write_text(SECRET)
dest = partial / 'dest'
dest.mkdir()
src = partial / 'src'
src.mkdir()
sub = src / 'sub'
sub.mkdir()
(src / 'f0').write_text("NEW-PUSHED-CONTENT\n")
base_rel = str(base).lstrip('/')
conf = write_daemon_conf(
[('everything', {'path': '/', 'read only': 'no', 'exclude': str(secret) + '/'})])
url = start_test_daemon(conf, DAEMON_PORT)
subprocess.run(
rsync_argv(
'-a',
f'--partial-dir=/{base_rel}/partial-dir-daemon/src/sub/../../../secret/',
f'{src}/',
f'{url}everything/{base_rel}/partial-dir-daemon/dest',
),
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
after = victim.read_text() if victim.exists() else None
if after is None:
test_fail("escaped: a --partial-dir '..' traversal DELETED the excluded victim")
if after != SECRET:
test_fail("escaped: a --partial-dir '..' traversal OVERWROTE the excluded victim "
f"({victim} is now {after!r})")
print("did not escape")