The refused set was built from the characters that obviously execute
something, and missed three that a SECOND shell acts on:
'!' negates in command position. A hook written as an access check --
`pre-xfer exec = sh -c '%RSYNC_USER_NAME% false'` -- becomes
`! false`, reports success, and serves the transfer. An
authenticated user named "!" turns a denial into an approval, which
is precisely the case the fail-closed comment above exists for.
'~' is tilde-expanded, so ~root becomes /root.
'{' and '}' brace-expand in bash and zsh.
None of them execute anything on their own, which is how a set built from
the obvious metacharacters came to miss them. That is also the standing
weakness of the approach: this is a deny-list, and the two rounds of
review it took to find '!' are the argument for eventually inverting it.
The documentation is corrected with it -- it claimed every shell-active
character was refused, which this disproves -- and now lists the set.
Each listed character is pinned by the test, which needed its module
paths to EXIST first: a missing path fails the transfer on its own, so
checking the exit status alone passed whether or not the character was
refused. Removing any single character from the set now fails the test.
(cherry picked from commit f704aa4aed)
Refusing a shell-active %VAR% in an exec hook is a real usability cost
and it is not confined to hostile input: the check runs on every
%RSYNC_*% value, so a module whose path holds a space cannot be
interpolated into a hook at all. `path = /srv/My Backups` with a command
mentioning %RSYNC_MODULE_PATH% refuses every transfer of that module,
with no attacker involved.
That is deliberate rather than an oversight. rsync escapes a
substitution for the quoting context it sits in, which is right for the
one shell that runs the command and wrong for a command that starts a
second one -- `sh -c '... %RSYNC_USER_NAME% ...'` hands the inner shell a
bare value, where "touc?" is glob-expanded against /usr/bin and chooses
the command rather than being data for it. Escaping for an unknown
number of passes is not possible, so the value is refused instead. Nor
can the check exempt operator-supplied values: `path` may itself be
templated from a peer one (`path = /home/%RSYNC_USER_NAME%`), and once
both are inside the same string rsync cannot tell them apart.
Say so in the manual, and give the way out: the same names are exported
to the command, so $RSYNC_MODULE_PATH inside a script is unrestricted.
The test pins both halves, since a documented behaviour with no oracle
drifts. Both are fault-injected: narrowing the refused set back makes
the interpolated half pass when the manual says it must not, and dropping
the RSYNC_MODULE_PATH export fails the workaround half.
(cherry picked from commit 5bf9940de4)
Move the uid/gid/acls/xattrs *_ndx assignments past check_batch_flags():
a mismatched-flag batch otherwise wrote F_XATTR(file) at offset 0 of
every file_struct, clobbering file->dirname. parse_negotiate_str() no
longer short-circuits on am_server, so each side picks its own #1 mutual
digest/checksum/compress choice rather than deferring to the peer's
order; man pages updated to match.
Co-authored-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
Document --insecure-links, clarify in rsyncd.conf(5) that exclude=/filter is a
name-based filter and not a symlink boundary, and record the backported fixes.
The daemon picks the challenge-response auth digest from the client's
negotiation, or -- for a peer that sends no digest list (any rsync before
3.2.0, including the openrsync that ships with macOS) -- falls back to md5
(protocol >= 30) or md4. The negotiation is unauthenticated, so an on-path
attacker can also rewrite it to force md5/md4 between two modern peers. A
captured (challenge, response) pair is then far cheaper to brute-force
offline against a weak digest, recovering a guessable shared secret.
Add a per-module (or global) "auth digest = NAME" parameter. After the
digest is negotiated, auth_server() refuses the connection (before issuing
the challenge) when the negotiated digest is weaker than NAME, comparing by
position in the strongest-first valid_auth_checksums list. Because the
check is on the resolved digest, it also catches the md5/md4 fallback, so an
old or list-omitting client cannot slip under the floor. A floor naming a
digest this build lacks is fail-closed. Default off, preserving
compatibility with older clients.
Documented in rsyncd.conf(5) and SECURITY.md. The test covers the strong-
client accept, the unknown-floor fail-closed, and -- with a bundled old
static rsync -- the real md5-downgrade refusal (plus a no-floor control).
(cherry picked from commit 1ba7951c; backport drops the SECURITY.md policy
section, which is 3.5.0-specific, and the _test.py, which the stable harness
does not run.)
- io: bound daemon argument lists so a malicious daemon client cannot grow
argv without limit (DoS);
- socket: bound the PROXY CONNECT request and proxy response header lines;
- daemon: require a trusted-proxy host list for "proxy protocol = true"
(reject untrusted proxy peers, fail-closed), and warn at startup when the
trusted-proxy list is unset so the fail-closed behaviour is not silent.
Tests: daemon-argv-limit, proxy-connect-request-too-long,
proxy-response-header-too-long, proxy-protocol-trusted-peer.
- The sanitize_paths variable was set too often. It only needs to be set
when the "inner" path is not "/". This change avoids sanitizing &
munging things for a path=/ module just because chroot is off.
- The default for "use chroot" is now "unset" instead of "true". When
unset it checks if chrooting works, and if not, it proceeds with a
sanitized copy instead of totally failing to work. This makes it
easier to setup a non-root rsync daemon, for instance. It will have
no effect on a typical Linux root-run daemon where the default will
continue to use chroot (because chrooting works). A config file can
explicitly set "use chroot = true | false" to force the choice.
- Try to improve the "use chroot" manpage.
- Add link targets for all option choices, not just the first one.
- Tweak cross-link arg format.
- Add more links, including some in the latest NEWS.
- Split out a few numbered lists.
The compression level of the first file in the transfer no longer sets
the level for all files that follow it. Document that per-file level
switching has no current effect (except for a global "dont compress = *"
rule in the daemon).
This is based on the long-standing patch but with the protocol changed
to just use newlines as delimiters instead of null chars (since names
should not contain a newline AND it makes it easier to write a helper
script). Lots of other small improvements and a better default value
for "numeric ids" when using "use chroot" with "name converter".
This removes the yodl dependency, which is sometimes hard to track down.
Instead, this uses a python3 script that leverages the cmarkgfm library
to turn the source file into html. Then, the script parses the html in
order to turn the tag stream into a nroff stream using a simple state
machine. While it's doing that it also implements one added format rule
that turns an ordinal list that starts at 0 into a description list
(since markdown doesn't have an easy description list idiom).