| 1 | #!/usr/bin/perl |
| 2 | # Name: /usr/local/bin/rrsync (should also have a symlink in /usr/bin) |
| 3 | # Purpose: Restricts rsync to subdirectory declared in .ssh/authorized_keys |
| 4 | # Author: Joe Smith <js-cgi@inwap.com> 30-Sep-2004 |
| 5 | # Modified by Wayne Davison <wayned@samba.org> 12-Jan-2005 |
| 6 | |
| 7 | use Socket; |
| 8 | use constant LOGFILE => 'rrsync.log'; |
| 9 | my $Usage = <<EOM; |
| 10 | Use 'command="$0 [-ro] SUBDIR"' |
| 11 | in front of lines in $ENV{HOME}/.ssh/authorized_keys |
| 12 | EOM |
| 13 | |
| 14 | my $ro = (@ARGV and $ARGV[0] eq '-ro') ? shift : ''; # -ro = Read-Only |
| 15 | my $subdir = shift; |
| 16 | die "No subdirectory specified\n$Usage" unless defined $subdir; |
| 17 | |
| 18 | # The client uses "rsync -av -e ssh src/ server:dir/", and sshd on the server |
| 19 | # executes this program when .ssh/authorized_keys has 'command="..."'. |
| 20 | # For example: |
| 21 | # command="rrsync logs/client" ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAzGhEeNlPr... |
| 22 | # command="rrsync -ro results" ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAmkHG1WCjC... |
| 23 | # |
| 24 | # Format of the envrionment variables set by sshd: |
| 25 | # SSH_ORIGINAL_COMMAND=rsync --server -vlogDtpr --partial . dir # push |
| 26 | # SSH_ORIGINAL_COMMAND=rsync --server --sender -vlogDtpr --partial . dir # pull |
| 27 | # SSH_CONNECTION=client_addr client_port server_port |
| 28 | |
| 29 | my $command = $ENV{SSH_ORIGINAL_COMMAND}; |
| 30 | die "Not invoked via sshd\n$Usage" unless defined $command; |
| 31 | die "SSH_ORIGINAL_COMMAND='$command' is not rsync\n" unless $command =~ /^rsync\s/; |
| 32 | die "$0 -ro: sending to read-only server not allowed\n" |
| 33 | if $ro and $command !~ /^rsync --server --sender /; |
| 34 | |
| 35 | my ($cmd,$dir) = $command =~ /^(rsync\s+(?:-[-a-zA-Z]+\s+)+\.) ?("[^"]*"|[^\s"]*)$/; |
| 36 | die "$0: invalid rsync-command syntax or options\n" if !defined $cmd; |
| 37 | |
| 38 | # Enforce default of $subdir instead of the normal $HOME default. |
| 39 | my $orig = $dir; |
| 40 | my @dirs; |
| 41 | $dir =~ s/^"(.*?)"$/$1/; |
| 42 | $dir =~ s/^\s+//; |
| 43 | $dir =~ s/\s+$//; |
| 44 | foreach (split(/(?<!\\)\s+/, $dir)) { |
| 45 | s/\\(\s)/$1/g; # Unescape any escaped whitespace |
| 46 | if ($subdir eq '/') { # Less checking for '/' access |
| 47 | $dir = '/' if $dir eq ''; |
| 48 | } else { |
| 49 | s#^/##; # Don't allow absolute paths |
| 50 | $_ = "$subdir/$_" unless m#^\Q$subdir\E(/|$)#; |
| 51 | 1 while s#/\.\.(/|$)#/__/#g; # Don't allow foo/../../etc |
| 52 | } |
| 53 | tr#-_/a-zA-Z0-9.,+@^%: #_#c; # Don't allow '"&;|!=()[]{}<>*?#\$ |
| 54 | s/(\s)/\\$1/g; # Re-escape whitespace |
| 55 | push(@dirs, $_); |
| 56 | } |
| 57 | push(@dirs, $subdir) unless @dirs; |
| 58 | $dir = join(' ', @dirs); |
| 59 | |
| 60 | if (-f LOGFILE and open LOG,'>>',LOGFILE) { |
| 61 | my ($mm,$hh) = (localtime)[1,2]; |
| 62 | my $host = $ENV{SSH_CONNECTION} || 'unknown'; |
| 63 | $host =~ s/ .*//; # Keep only the client's IP addr |
| 64 | $host =~ s/^::ffff://; |
| 65 | $host = gethostbyaddr(inet_aton($host),AF_INET) || $host; |
| 66 | my $dir_result = $dir eq $orig ? " OK" : "> \"$dir\""; |
| 67 | printf LOG "%02d:%02d %-13s [%s] =%s\n", $hh, $mm, $host, $command, $dir_result; |
| 68 | close LOG; |
| 69 | } |
| 70 | |
| 71 | exec "$cmd \"$dir\"" or die "exec($cmd \"$dir\") failed: $? $!"; |
| 72 | # Note: This assumes that the rsync protocol will not be maliciously hijacked. |