The branch, master has been updated via 72adf49b rrsync improvements from 73ceea6a Convert atomic-rsync to python.
https://git.samba.org/?p=rsync.git;a=shortlog;h=master - Log ----------------------------------------------------------------- commit 72adf49ba8cb81426e2b9799fbd43c6284b013a9 Author: Wayne Davison <wa...@opencoder.net> Date: Sun Dec 26 12:29:00 2021 -0800 rrsync improvements - Convert rrsync to python. - Enhance security of arg & option checking. - Reject `-L` (`--copy-links`) by default. - Add `-munge` and `-no-del` options. - Tweak the logfile line format. - Created an rrsync man page. - Use `configure --with-rrsync` if you want `make install` to install rrsync and its man page. - Give lsh more rrsync testing support. ----------------------------------------------------------------------- Summary of changes: Makefile.in | 10 +- NEWS.md | 18 +- configure.ac | 7 + maybe-make-man | 2 +- md2man | 13 +- packaging/cull_options | 36 +-- support/lsh | 12 +- support/rrsync | 595 +++++++++++++++++++++++++++---------------------- support/rrsync.1.md | 89 ++++++++ 9 files changed, 488 insertions(+), 294 deletions(-) create mode 100644 support/rrsync.1.md Changeset truncated at 500 lines: diff --git a/Makefile.in b/Makefile.in index 3c8c2240..5eed339e 100644 --- a/Makefile.in +++ b/Makefile.in @@ -6,6 +6,7 @@ exec_prefix=@exec_prefix@ bindir=@bindir@ libdir=@libdir@/rsync mandir=@mandir@ +with_rrsync=@with_rrsync@ LIBS=@LIBS@ CC=@CC@ @@ -80,6 +81,10 @@ install: all if test -f rsync.1; then $(INSTALLMAN) -m 644 rsync.1 $(DESTDIR)$(mandir)/man1; fi if test -f rsync-ssl.1; then $(INSTALLMAN) -m 644 rsync-ssl.1 $(DESTDIR)$(mandir)/man1; fi if test -f rsyncd.conf.5; then $(INSTALLMAN) -m 644 rsyncd.conf.5 $(DESTDIR)$(mandir)/man5; fi + if test "$(with_rrsync)" = yes; then \ + $(INSTALLCMD) -m 755 $(srcdir)/support/rrsync $(DESTDIR)$(bindir); \ + if test -f rrsync.1; then $(INSTALLMAN) -m 644 rrsync.1 $(DESTDIR)$(mandir)/man1; fi; \ + fi install-ssl-daemon: stunnel-rsyncd.conf -$(MKDIR_P) $(DESTDIR)/etc/stunnel @@ -247,7 +252,7 @@ proto.h-tstamp: $(srcdir)/*.c $(srcdir)/lib/compat.c daemon-parm.h $(AWK) -f $(srcdir)/mkproto.awk $(srcdir)/*.c $(srcdir)/lib/compat.c daemon-parm.h .PHONY: man -man: rsync.1 rsync-ssl.1 rsyncd.conf.5 +man: rsync.1 rsync-ssl.1 rsyncd.conf.5 rrsync.1 rsync.1: rsync.1.md md2man version.h Makefile @$(srcdir)/maybe-make-man $(srcdir) rsync.1.md @@ -258,6 +263,9 @@ rsync-ssl.1: rsync-ssl.1.md md2man version.h Makefile rsyncd.conf.5: rsyncd.conf.5.md md2man version.h Makefile @$(srcdir)/maybe-make-man $(srcdir) rsyncd.conf.5.md +rrsync.1: support/rrsync.1.md md2man Makefile + @$(srcdir)/maybe-make-man $(srcdir) support/rrsync.1.md + .PHONY: clean clean: cleantests rm -f *~ $(OBJS) $(CHECK_PROGS) $(CHECK_OBJS) $(CHECK_SYMLINKS) \ diff --git a/NEWS.md b/NEWS.md index eaa82b39..b3002e89 100644 --- a/NEWS.md +++ b/NEWS.md @@ -97,9 +97,15 @@ - More ASM optimizations from Shark64. - - Make rrsync pass --munge-links to rsync by default to make the restricted - dir extra safe (with an option to turn it off if you trust your users). - Also updated the known options list. + - Transformed rrsync into a python script with improvements: security has been + beefed up; the known rsync options were updated to include recent additions; + rrsync rejects `-L` (`--copy-links`) by default to make it harder to exploit + any out-of-subdir symlinks; a new rrsync option of `-munge` tells rrsync to + always enable the `--munge-links` rsync option on the server side; a new + rrsync option of `-no-del` disables all `--remove*` and `--delete*` rsync + options on the server side; the log format has been tweaked slightly to add + seconds to the timestamp and output the command executed as a tuple; an + rrsync.1 manpage is now created. - Work around a glibc bug where lchmod() breaks in a chroot w/o /proc mounted. @@ -107,6 +113,12 @@ ### PACKAGING RELATED: + - Give configure the --with-rrsync option if you want `make install` to + install the (now python3) rrsync script and its (new) man page. + + - If the rrsync script is installed, make its package depend on python3 and + (suggested but not required) the python3 braceexpand lib. + - When creating a package from a non-release version (w/o a git checkout), the packager can elect to create git-version.h and define RSYNC_GITVER to the string they want `--version` to output. (The file is still auto-generated diff --git a/configure.ac b/configure.ac index 9e7338cf..84111de8 100644 --- a/configure.ac +++ b/configure.ac @@ -136,6 +136,13 @@ if test x"$GCC" = x"yes"; then CFLAGS="$CFLAGS -Wall -W" fi +AC_ARG_WITH(rrsync, + AS_HELP_STRING([--with-rrsync],[also install the rrsync script and its man page])) +if test x"$with_rrsync" != x"yes"; then + with_rrsync=no +fi +AC_SUBST(with_rrsync) + AC_ARG_WITH(included-popt, AS_HELP_STRING([--with-included-popt],[use bundled popt library, not from system])) diff --git a/maybe-make-man b/maybe-make-man index b7f0a9f1..59f2dce4 100755 --- a/maybe-make-man +++ b/maybe-make-man @@ -37,4 +37,4 @@ if [ ! -f "$flagfile" ]; then fi fi -"$srcdir/md2man" "$srcdir/$inname" +"$srcdir/md2man" -s "$srcdir" "$srcdir/$inname" diff --git a/md2man b/md2man index fa1d2e82..fd546f19 100755 --- a/md2man +++ b/md2man @@ -85,7 +85,9 @@ def main(): die('Failed to parse NAME.NUM.md out of input file:', args.mdfile) fi = argparse.Namespace(**fi.groupdict()) - if not fi.srcdir: + if args.srcdir: + fi.srcdir = args.srcdir + '/' + elif not fi.srcdir: fi.srcdir = './' fi.title = fi.prog + '(' + fi.sect + ') man page' @@ -105,7 +107,7 @@ def main(): for fn in (fi.srcdir + 'version.h', 'Makefile'): try: st = os.lstat(fn) - except: + except OSError: die('Failed to find', fi.srcdir + fn) if not fi.mtime: fi.mtime = st.st_mtime @@ -129,6 +131,10 @@ def main(): if var == 'srcdir': break + fi.prog_ver = 'rsync ' + env_subs['VERSION'] + if fi.prog != 'rsync': + fi.prog_ver = fi.prog + ' from ' + fi.prog_ver + with open(fi.fn, 'r', encoding='utf-8') as fh: txt = fh.read() @@ -140,7 +146,7 @@ def main(): txt = None fi.date = time.strftime('%d %b %Y', time.localtime(fi.mtime)) - fi.man_headings = (fi.prog, fi.sect, fi.date, fi.prog + ' ' + env_subs['VERSION'], env_subs['prefix']) + fi.man_headings = (fi.prog, fi.sect, fi.date, fi.prog_ver, env_subs['prefix']) HtmlToManPage(fi) @@ -374,6 +380,7 @@ def die(*msg): if __name__ == '__main__': parser = argparse.ArgumentParser(description='Transform a NAME.NUM.md markdown file into a NAME.NUM.html web page & a NAME.NUM man page.', add_help=False) + parser.add_argument('--srcdir', '-s', help='Specify the source dir if the input file is not in it.') parser.add_argument('--test', action='store_true', help='Test if we can parse the input w/o updating any files.') parser.add_argument('--debug', '-D', action='count', default=0, help='Output copious info on the html parsing. Repeat for even more.') parser.add_argument("--help", "-h", action="help", help="Output this help message and exit.") diff --git a/packaging/cull_options b/packaging/cull_options index 85311c7c..d4e1c626 100755 --- a/packaging/cull_options +++ b/packaging/cull_options @@ -7,7 +7,7 @@ import re, argparse short_no_arg = { } short_with_num = { '@': 1 }; -long_opt = { # These include some extra long-args that BackupPC uses: +long_opts = { # These include some extra long-args that BackupPC uses: 'block-size': 1, 'daemon': -1, 'debug': 1, @@ -25,6 +25,7 @@ long_opt = { # These include some extra long-args that BackupPC uses: 'owner': 0, 'perms': 0, 'recursive': 0, + 'stderr': 1, 'times': 0, 'write-devices': -1, } @@ -49,8 +50,8 @@ def main(): m = re.search(r'args\[ac\+\+\] = "--([^"=]+)"', line) if m: last_long_opt = m.group(1) - if last_long_opt not in long_opt: - long_opt[last_long_opt] = 0 + if last_long_opt not in long_opts: + long_opts[last_long_opt] = 0 else: last_long_opt = None continue @@ -58,13 +59,13 @@ def main(): if last_long_opt: m = re.search(r'args\[ac\+\+\] = ([^["\s]+);', line) if m: - long_opt[last_long_opt] = 2 + long_opts[last_long_opt] = 2 last_long_opt = None continue m = re.search(r'return "--([^"]+-dest)";', line) if m: - long_opt[m.group(1)] = 2 + long_opts[m.group(1)] = 2 last_long_opt = None continue @@ -74,19 +75,18 @@ def main(): if not m: m = re.search(r'fmt = .*: "--([^"=]+)=', line) if m: - long_opt[m.group(1)] = 1 + long_opts[m.group(1)] = 1 last_long_opt = None - long_opt['files-from'] = 3 + long_opts['files-from'] = 3 - txt = """ -# These options are the only options that rsync might send to the server, -# and only in the option format that the stock rsync produces. + txt = """\ +### START of options data produced by the cull_options script. ### # To disable a short-named option, add its letter to this string: """ - txt += str_assign('short_disabled', 's') + "\n" + txt += str_assign('short_disabled', 'Ls') + "\n" txt += str_assign('short_no_arg', ''.join(sorted(short_no_arg)), 'DO NOT REMOVE ANY') txt += str_assign('short_with_num', ''.join(sorted(short_with_num)), 'DO NOT REMOVE ANY') @@ -99,24 +99,24 @@ def main(): print(txt, end='') if args.python: - print("long_opt = {") + print("long_opts = {") sep = ':' else: print("our %long_opt = (") sep = ' =>' - for opt in sorted(long_opt): + for opt in sorted(long_opts): if opt.startswith(('min-', 'max-')): val = 1 else: - val = long_opt[opt] + val = long_opts[opt] print(' ', repr(opt) + sep, str(val) + ',') if args.python: print("}") else: print(");") - print('') + print("\n### END of options data produced by the cull_options script. ###") def str_assign(name, val, comment=None): @@ -129,10 +129,12 @@ def str_assign(name, val, comment=None): if __name__ == '__main__': parser = argparse.ArgumentParser(description="Output culled rsync options for rrsync.", add_help=False) out_group = parser.add_mutually_exclusive_group() - out_group.add_argument('--perl', action='store_true', help="Output perl code (the default).") - out_group.add_argument('--python', action='store_true', help="Output python code.") + out_group.add_argument('--perl', action='store_true', help="Output perl code.") + out_group.add_argument('--python', action='store_true', help="Output python code (the default).") parser.add_argument('--help', '-h', action='help', help="Output this help message and exit.") args = parser.parse_args() + if not args.perl: + args.python = True main() # vim: sw=4 et diff --git a/support/lsh b/support/lsh index ebfe898c..40fe3d73 100755 --- a/support/lsh +++ b/support/lsh @@ -18,6 +18,8 @@ GetOptions( 'rrsync=s' => \( my $rrsync_dir ), 'ro' => \( my $rrsync_ro = '' ), 'wo' => \( my $rrsync_wo = '' ), + 'munge' => \( my $rrsync_munge = '' ), + 'no-del' => \( my $rrsync_no_del = '' ), ) or &usage; &usage unless @ARGV > 1; @@ -71,16 +73,12 @@ unless ($no_chdir) { } if ($rrsync_dir) { - my $cmd = ''; - foreach (@ARGV) { - (my $arg = $_) =~ s/(['";|()\[\]{}\$!*?<> \t&~\\])/\\$1/g; - $cmd .= ' ' . $arg; - } - $cmd =~ s/^\s+//; - $ENV{SSH_ORIGINAL_COMMAND} = $cmd; + $ENV{SSH_ORIGINAL_COMMAND} = join(' ', @ARGV); push @cmd, 'rrsync'; push @cmd, '-ro' if $rrsync_ro; push @cmd, '-wo' if $rrsync_wo; + push @cmd, '-munge' if $rrsync_munge; + push @cmd, '-no-del' if $rrsync_no_del; push @cmd, $rrsync_dir; } else { push @cmd, '/bin/sh', '-c', "@ARGV"; diff --git a/support/rrsync b/support/rrsync index 4c5dd2aa..5b43a819 100755 --- a/support/rrsync +++ b/support/rrsync @@ -1,282 +1,353 @@ -#!/usr/bin/env perl -# Name: /usr/local/bin/rrsync (should also have a symlink in /usr/bin) -# Purpose: Restricts rsync to subdirectory declared in .ssh/authorized_keys -# Author: Joe Smith <js-...@inwap.com> 30-Sep-2004 -# Modified by: Wayne Davison <wa...@opencoder.net> -use strict; - -use Socket; -use Cwd 'abs_path'; -use File::Glob ':glob'; - -# You may configure these values to your liking. See also the section -# of options if you want to disable any options that rsync accepts. -use constant RSYNC => '/usr/bin/rsync'; -use constant LOGFILE => 'rrsync.log'; - -my $Usage = <<EOM; -Use 'command="$0 [-ro|-wo|-no-munge] SUBDIR"' - in front of lines in $ENV{HOME}/.ssh/authorized_keys -EOM - -# Handle the -ro, -wo, & -no-munge options. -our $only = ''; -our $force_munge = 1; -while (@ARGV) { - if ($ARGV[0] =~ /^-([rw])o$/) { - my $r_or_w = $1; - if ($only && $only ne $r_or_w) { - die "$0: the -ro and -wo options conflict.\n"; - } - $only = $r_or_w; - } elsif ($ARGV[0] eq '-no-munge') { - $force_munge = 0; - } else { - last; - } - shift; -} +#!/usr/bin/env python3 -our $subdir = shift; -die "$0: No subdirectory specified\n$Usage" unless defined $subdir; -$subdir = abs_path($subdir); -die "$0: Restricted directory does not exist!\n" if $subdir ne '/' && !-d $subdir; - -# The client uses "rsync -av -e ssh src/ server:dir/", and sshd on the server -# executes this program when .ssh/authorized_keys has 'command="..."'. -# For example: -# command="rrsync logs/client" ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAzGhEeNlPr... -# command="rrsync -ro results" ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAmkHG1WCjC... -# -# Format of the environment variables set by sshd: -# SSH_ORIGINAL_COMMAND=rsync --server -vlogDtpr --partial . ARG # push -# SSH_ORIGINAL_COMMAND=rsync --server --sender -vlogDtpr --partial . ARGS # pull -# SSH_CONNECTION=client_addr client_port server_port - -my $command = $ENV{SSH_ORIGINAL_COMMAND}; -die "$0: Not invoked via sshd\n$Usage" unless defined $command; -die "$0: SSH_ORIGINAL_COMMAND='$command' is not rsync\n" unless $command =~ s/^rsync\s+//; -die "$0: --server option is not first\n" unless $command =~ /^--server\s/; -our $am_sender = $command =~ /^--server\s+--sender\s/; # Restrictive on purpose! -die "$0 sending to read-only server not allowed\n" if $only eq 'r' && !$am_sender; -die "$0 reading from write-only server not allowed\n" if $only eq 'w' && $am_sender; +# Restricts rsync to subdirectory declared in .ssh/authorized_keys. See +# the rrsync man page for details of how to make use of this script. -### START of options data produced by the cull_options script. ### +# NOTE: install python3 braceexpand to support brace expansion in the args! + +# Originally a perl script by: Joe Smith <js-...@inwap.com> 30-Sep-2004 +# Python version by: Wayne Davison <wa...@opencoder.net> + +# You may configure these 2 values to your liking. See also the section of +# short & long options if you want to disable any options that rsync accepts. +RSYNC = '/usr/bin/rsync' +LOGFILE = 'rrsync.log' # NOTE: the file must exist for a line to be appended! + +# The following options are mainly the options that a client rsync can send +# to the server, and usually just in the one option format that the stock +# rsync produces. However, there are some additional convenience options +# added as well, and thus a few options are present in both the short and +# long lists (such as --group, --owner, and --perms). -# These options are the only options that rsync might send to the server, -# and only in the option format that the stock rsync produces. +# NOTE when disabling: check for both a short & long version of the option! + +### START of options data produced by the cull_options script. ### # To disable a short-named option, add its letter to this string: -our $short_disabled = 's'; +short_disabled = 'Ls' -our $short_no_arg = 'ACDEHIJKLNORSUWXbcdgklmnopqrstuvxyz'; # DO NOT REMOVE ANY -our $short_with_num = '@B'; # DO NOT REMOVE ANY +short_no_arg = 'ACDEHIJKLNORSUWXbcdgklmnopqrstuvxyz' # DO NOT REMOVE ANY +short_with_num = '@B' # DO NOT REMOVE ANY # To disable a long-named option, change its value to a -1. The values mean: # 0 = the option has no arg; 1 = the arg doesn't need any checking; 2 = only # check the arg when receiving; and 3 = always check the arg. -our %long_opt = ( - 'append' => 0, - 'backup-dir' => 2, - 'block-size' => 1, - 'bwlimit' => 1, - 'checksum-choice' => 1, - 'checksum-seed' => 1, - 'compare-dest' => 2, - 'compress-choice' => 1, - 'compress-level' => 1, - 'copy-dest' => 2, - 'copy-unsafe-links' => 0, - 'daemon' => -1, - 'debug' => 1, - 'delay-updates' => 0, - 'delete' => 0, - 'delete-after' => 0, - 'delete-before' => 0, - 'delete-delay' => 0, - 'delete-during' => 0, - 'delete-excluded' => 0, - 'delete-missing-args' => 0, - 'existing' => 0, - 'fake-super' => 0, - 'files-from' => 3, - 'force' => 0, - 'from0' => 0, - 'fsync' => 2, - 'fuzzy' => 0, - 'group' => 0, - 'groupmap' => 1, - 'hard-links' => 0, - 'iconv' => 1, - 'ignore-errors' => 0, - 'ignore-existing' => 0, - 'ignore-missing-args' => 0, - 'ignore-times' => 0, - 'info' => 1, - 'inplace' => 0, - 'link-dest' => 2, - 'links' => 0, - 'list-only' => 0, - 'log-file' => 3, - 'log-format' => 1, - 'max-alloc' => 1, - 'max-delete' => 1, - 'max-size' => 1, - 'min-size' => 1, - 'mkpath' => 0, - 'modify-window' => 1, - 'msgs2stderr' => 0, - 'munge-links' => 0, - 'new-compress' => 0, - 'no-W' => 0, - 'no-implied-dirs' => 0, - 'no-msgs2stderr' => 0, - 'no-munge-links' => -1, - 'no-r' => 0, - 'no-relative' => 0, - 'no-specials' => 0, - 'numeric-ids' => 0, - 'old-compress' => 0, - 'one-file-system' => 0, - 'only-write-batch' => 1, - 'open-noatime' => 0, - 'owner' => 0, - 'partial' => 0, - 'partial-dir' => 2, - 'perms' => 0, - 'preallocate' => 0, - 'recursive' => 0, - 'remove-sent-files' => 0, - 'remove-source-files' => 0, - 'safe-links' => 0, - 'sender' => 0, - 'server' => 0, - 'size-only' => 0, - 'skip-compress' => 1, - 'specials' => 0, - 'stats' => 0, - 'suffix' => 1, - 'super' => 0, - 'temp-dir' => 2, - 'timeout' => 1, - 'times' => 0, - 'use-qsort' => 0, -- The rsync repository. _______________________________________________ rsync-cvs mailing list rsync-cvs@lists.samba.org https://lists.samba.org/mailman/listinfo/rsync-cvs