| SECURITY [CAN-2005-0448]: |
| |
| Rewrite File::Path::rmtree to avoid race condition which allows an |
| attacker with write permission on directories in the tree being |
| removed to make files setuid or to remove arbitrary files (see |
| http://bugs.debian.org/286905 and http://bugs.debian.org/286922). |
| |
| Revised for 5.8.8-7etch6 to avoid failure when rmtree removes the |
| current directory. |
| |
| diff --git a/lib/File/Path.pm b/lib/File/Path.pm |
| index 2e41ff3..fd32b96 100644 |
| --- a/lib/File/Path.pm |
| +++ b/lib/File/Path.pm |
| @@ -72,33 +72,17 @@ or C<unlink> to remove it, or that it's skipping it. |
| |
| =item * |
| |
| -a boolean value, which if TRUE will cause C<rmtree> to |
| -skip any files to which you do not have delete access |
| -(if running under VMS) or write access (if running |
| -under another OS). This will change in the future when |
| -a criterion for 'delete permission' under OSs other |
| -than VMS is settled. (defaults to FALSE) |
| +a boolean value, which if FALSE (the default for non-root users) will |
| +cause C<rmtree> to adjust the mode of directories (if required) prior |
| +to attempting to remove the contents. Note that on interruption or |
| +failure of C<rmtree>, directories may be left with more permissive |
| +modes for the owner. |
| |
| =back |
| |
| It returns the number of files successfully deleted. Symlinks are |
| simply deleted and not followed. |
| |
| -B<NOTE:> There are race conditions internal to the implementation of |
| -C<rmtree> making it unsafe to use on directory trees which may be |
| -altered or moved while C<rmtree> is running, and in particular on any |
| -directory trees with any path components or subdirectories potentially |
| -writable by untrusted users. |
| - |
| -Additionally, if the third parameter is not TRUE and C<rmtree> is |
| -interrupted, it may leave files and directories with permissions altered |
| -to allow deletion (and older versions of this module would even set |
| -files and directories to world-read/writable!) |
| - |
| -Note also that the occurrence of errors in C<rmtree> can be determined I<only> |
| -by trapping diagnostic messages using C<$SIG{__WARN__}>; it is not apparent |
| -from the return value. |
| - |
| =head1 DIAGNOSTICS |
| |
| =over 4 |
| @@ -172,111 +156,127 @@ sub mkpath { |
| @created; |
| } |
| |
| -sub rmtree { |
| - my($roots, $verbose, $safe) = @_; |
| - my(@files); |
| - my($count) = 0; |
| - $verbose ||= 0; |
| - $safe ||= 0; |
| +sub _rmtree; |
| +sub _rmtree |
| +{ |
| + my ($path, $prefix, $up, $up_dev, $up_ino, $verbose, $safe) = @_; |
| + my $up_name = $up eq '..' ? 'parent' : 'initial'; |
| + |
| + my ($dev, $ino) = lstat $path or return 0; |
| + unless (-d _) |
| + { |
| + print "unlink $prefix$path\n" if $verbose; |
| + unless (unlink $path) |
| + { |
| + carp "Can't remove file $prefix$path ($!)"; |
| + return 0; |
| + } |
| |
| - if ( defined($roots) && length($roots) ) { |
| - $roots = [$roots] unless ref $roots; |
| + return 1; |
| } |
| - else { |
| - carp "No root path(s) specified\n"; |
| - return 0; |
| + |
| + unless (chdir $path) |
| + { |
| + carp "Can't chdir to $prefix$path ($!)"; |
| + return 0; |
| } |
| |
| - my($root); |
| - foreach $root (@{$roots}) { |
| - if ($Is_MacOS) { |
| - $root = ":$root" if $root !~ /:/; |
| - $root =~ s#([^:])\z#$1:#; |
| - } else { |
| - $root =~ s#/\z##; |
| - } |
| - (undef, undef, my $rp) = lstat $root or next; |
| - $rp &= 07777; # don't forget setuid, setgid, sticky bits |
| - if ( -d _ ) { |
| - # notabene: 0700 is for making readable in the first place, |
| - # it's also intended to change it to writable in case we have |
| - # to recurse in which case we are better than rm -rf for |
| - # subtrees with strange permissions |
| - chmod($rp | 0700, ($Is_VMS ? VMS::Filespec::fileify($root) : $root)) |
| - or carp "Can't make directory $root read+writeable: $!" |
| - unless $safe; |
| - |
| - if (opendir my $d, $root) { |
| - no strict 'refs'; |
| - if (!defined ${"\cTAINT"} or ${"\cTAINT"}) { |
| - # Blindly untaint dir names |
| - @files = map { /^(.*)$/s ; $1 } readdir $d; |
| - } else { |
| - @files = readdir $d; |
| - } |
| - closedir $d; |
| - } |
| - else { |
| - carp "Can't read $root: $!"; |
| - @files = (); |
| - } |
| + # avoid a race condition where a directory may be replaced by a |
| + # symlink between the lstat and the chdir |
| + my ($new_dev, $new_ino, $perm) = stat '.'; |
| + unless ("$new_dev:$new_ino" eq "$dev:$ino") |
| + { |
| + croak "Directory $prefix$path changed before chdir, aborting"; |
| + } |
| |
| - # Deleting large numbers of files from VMS Files-11 filesystems |
| - # is faster if done in reverse ASCIIbetical order |
| - @files = reverse @files if $Is_VMS; |
| - ($root = VMS::Filespec::unixify($root)) =~ s#\.dir\z## if $Is_VMS; |
| - if ($Is_MacOS) { |
| - @files = map("$root$_", @files); |
| - } else { |
| - @files = map("$root/$_", grep $_!~/^\.{1,2}\z/s,@files); |
| - } |
| - $count += rmtree(\@files,$verbose,$safe); |
| - if ($safe && |
| - ($Is_VMS ? !&VMS::Filespec::candelete($root) : !-w $root)) { |
| - print "skipped $root\n" if $verbose; |
| - next; |
| - } |
| - chmod $rp | 0700, $root |
| - or carp "Can't make directory $root writeable: $!" |
| - if $force_writeable; |
| - print "rmdir $root\n" if $verbose; |
| - if (rmdir $root) { |
| - ++$count; |
| - } |
| - else { |
| - carp "Can't remove directory $root: $!"; |
| - chmod($rp, ($Is_VMS ? VMS::Filespec::fileify($root) : $root)) |
| - or carp("and can't restore permissions to " |
| - . sprintf("0%o",$rp) . "\n"); |
| - } |
| - } |
| - else { |
| - if ($safe && |
| - ($Is_VMS ? !&VMS::Filespec::candelete($root) |
| - : !(-l $root || -w $root))) |
| - { |
| - print "skipped $root\n" if $verbose; |
| - next; |
| - } |
| - chmod $rp | 0600, $root |
| - or carp "Can't make file $root writeable: $!" |
| - if $force_writeable; |
| - print "unlink $root\n" if $verbose; |
| - # delete all versions under VMS |
| - for (;;) { |
| - unless (unlink $root) { |
| - carp "Can't unlink file $root: $!"; |
| - if ($force_writeable) { |
| - chmod $rp, $root |
| - or carp("and can't restore permissions to " |
| - . sprintf("0%o",$rp) . "\n"); |
| - } |
| - last; |
| - } |
| - ++$count; |
| - last unless $Is_VMS && lstat $root; |
| - } |
| + $perm &= 07777; |
| + my $nperm = $perm | 0700; |
| + unless ($safe or $nperm == $perm or chmod $nperm, '.') |
| + { |
| + carp "Can't make directory $prefix$path read+writeable ($!)"; |
| + $nperm = $perm; |
| + } |
| + |
| + my $count = 0; |
| + if (opendir my $dir, '.') |
| + { |
| + my $entry; |
| + while (defined ($entry = readdir $dir)) |
| + { |
| + next if $entry =~ /^\.\.?$/; |
| + $entry =~ /^(.*)$/s; $entry = $1; # untaint |
| + $count += _rmtree $entry, "$prefix$path/", '..', $dev, $ino, |
| + $verbose, $safe; |
| } |
| + |
| + closedir $dir; |
| + } |
| + |
| + # restore directory permissions if required (in case the rmdir |
| + # below fails) now, while we're still in the directory and may do |
| + # so without a race via '.' |
| + unless ($nperm == $perm or chmod $perm, '.') |
| + { |
| + carp "Can't restore permissions on directory $prefix$path ($!)"; |
| + } |
| + |
| + # don't leave the caller in an unexpected directory |
| + unless (chdir $up) |
| + { |
| + croak "Can't return to $up_name directory from $prefix$path ($!)"; |
| + } |
| + |
| + # ensure that a chdir .. didn't take us somewhere other than |
| + # where we expected (see CVE-2002-0435) |
| + unless (($new_dev, $new_ino) = stat '.' |
| + and "$new_dev:$new_ino" eq "$up_dev:$up_ino") |
| + { |
| + croak "\u$up_name directory changed since entering $prefix$path"; |
| + } |
| + |
| + print "rmdir $prefix$path\n" if $verbose; |
| + if (rmdir $path) |
| + { |
| + $count++; |
| + } |
| + else |
| + { |
| + carp "Can't remove directory $prefix$path ($!)"; |
| + } |
| + |
| + return $count; |
| +} |
| + |
| +sub rmtree |
| +{ |
| + my ($p, $verbose, $safe) = @_; |
| + $p = [] unless defined $p and length $p; |
| + $p = [ $p ] unless ref $p; |
| + my @paths = grep defined && length, @$p; |
| + |
| + # default to "unsafe" for non-root (will chmod dirs) |
| + $safe = $> ? 0 : 1 unless defined $safe; |
| + |
| + unless (@paths) |
| + { |
| + carp "No root path(s) specified"; |
| + return; |
| + } |
| + |
| + opendir my $oldpwd, '.' or do { |
| + carp "Can't fetch initial working directory"; |
| + return; |
| + }; |
| + |
| + my ($dev, $ino) = stat '.' or do { |
| + carp "Can't stat initial working directory"; |
| + return; |
| + }; |
| + |
| + my $count = 0; |
| + for my $path (@paths) |
| + { |
| + $count += _rmtree $path, '', $oldpwd, $dev, $ino, $verbose, $safe; |
| } |
| |
| $count; |