diff -Nru dpkg-1.21.1ubuntu2/debian/changelog dpkg-1.21.1ubuntu2.1/debian/changelog --- dpkg-1.21.1ubuntu2/debian/changelog 2022-04-06 09:40:25.000000000 +0000 +++ dpkg-1.21.1ubuntu2.1/debian/changelog 2022-05-25 11:11:51.000000000 +0000 @@ -1,3 +1,12 @@ +dpkg (1.21.1ubuntu2.1) jammy-security; urgency=medium + + * SECURITY UPDATE: Directory traversal issue in dpkg-source + - scripts/Dpkg/Source/Archive.pm, scripts/t/Dpkg_Source_Archive.t: + Prevent directory traversal for in-place extracts. + - CVE-2022-1664 + + -- Marc Deslauriers Wed, 25 May 2022 07:11:51 -0400 + dpkg (1.21.1ubuntu2) jammy; urgency=medium * Don't install dpkg-fsys-usrunmess script. (LP: #1968022) diff -Nru dpkg-1.21.1ubuntu2/scripts/Dpkg/Source/Archive.pm dpkg-1.21.1ubuntu2.1/scripts/Dpkg/Source/Archive.pm --- dpkg-1.21.1ubuntu2/scripts/Dpkg/Source/Archive.pm 2021-09-11 18:35:03.000000000 +0000 +++ dpkg-1.21.1ubuntu2.1/scripts/Dpkg/Source/Archive.pm 2022-05-25 11:11:46.000000000 +0000 @@ -21,9 +21,11 @@ our $VERSION = '0.01'; use Carp; +use Errno qw(ENOENT); use File::Temp qw(tempdir); use File::Basename qw(basename); use File::Spec; +use File::Find; use Cwd; use Dpkg (); @@ -110,19 +112,13 @@ my %spawn_opts = (wait_child => 1); # Prepare destination - my $tmp; - if ($opts{in_place}) { - $spawn_opts{chdir} = $dest; - $tmp = $dest; # So that fixperms call works - } else { - my $template = basename($self->get_filename()) . '.tmp-extract.XXXXX'; - unless (-e $dest) { - # Kludge so that realpath works - mkdir($dest) or syserr(g_('cannot create directory %s'), $dest); - } - $tmp = tempdir($template, DIR => Cwd::realpath("$dest/.."), CLEANUP => 1); - $spawn_opts{chdir} = $tmp; + my $template = basename($self->get_filename()) . '.tmp-extract.XXXXX'; + unless (-e $dest) { + # Kludge so that realpath works + mkdir($dest) or syserr(g_('cannot create directory %s'), $dest); } + my $tmp = tempdir($template, DIR => Cwd::realpath("$dest/.."), CLEANUP => 1); + $spawn_opts{chdir} = $tmp; # Prepare stuff that handles the input of tar $self->ensure_open('r', delete_sig => [ 'PIPE' ]); @@ -145,22 +141,94 @@ # have to be calculated using mount options and other madness. fixperms($tmp) unless $opts{no_fixperms}; - # Stop here if we extracted in-place as there's nothing to move around - return if $opts{in_place}; - - # Rename extracted directory - opendir(my $dir_dh, $tmp) or syserr(g_('cannot opendir %s'), $tmp); - my @entries = grep { $_ ne '.' && $_ ne '..' } readdir($dir_dh); - closedir($dir_dh); - my $done = 0; - erasedir($dest); - if (scalar(@entries) == 1 && ! -l "$tmp/$entries[0]" && -d _) { - rename("$tmp/$entries[0]", $dest) - or syserr(g_('unable to rename %s to %s'), - "$tmp/$entries[0]", $dest); + # If we are extracting "in-place" do not remove the destination directory. + if ($opts{in_place}) { + my $canon_basedir = Cwd::realpath($dest); + # On Solaris /dev/null points to /devices/pseudo/mm@0:null. + my $canon_devnull = Cwd::realpath('/dev/null'); + my $check_symlink = sub { + my $pathname = shift; + my $canon_pathname = Cwd::realpath($pathname); + if (not defined $canon_pathname) { + return if $! == ENOENT; + + syserr(g_("pathname '%s' cannot be canonicalized"), $pathname); + } + return if $canon_pathname eq $canon_devnull; + return if $canon_pathname eq $canon_basedir; + return if $canon_pathname =~ m{^\Q$canon_basedir/\E}; + warning(g_("pathname '%s' points outside source root (to '%s')"), + $pathname, $canon_pathname); + }; + + my $move_in_place = sub { + my $relpath = File::Spec->abs2rel($File::Find::name, $tmp); + my $destpath = File::Spec->catfile($dest, $relpath); + + my ($mode, $atime, $mtime); + lstat $File::Find::name + or syserr(g_('cannot get source pathname %s metadata'), $File::Find::name); + ((undef) x 2, $mode, (undef) x 5, $atime, $mtime) = lstat _; + my $src_is_dir = -d _; + + my $dest_exists = 1; + if (not lstat $destpath) { + if ($! == ENOENT) { + $dest_exists = 0; + } else { + syserr(g_('cannot get target pathname %s metadata'), $destpath); + } + } + my $dest_is_dir = -d _; + if ($dest_exists) { + if ($dest_is_dir && $src_is_dir) { + # Refresh the destination directory attributes with the + # ones from the tarball. + chmod $mode, $destpath + or syserr(g_('cannot change directory %s mode'), $File::Find::name); + utime $atime, $mtime, $destpath + or syserr(g_('cannot change directory %s times'), $File::Find::name); + + # We should do nothing, and just walk further tree. + return; + } elsif ($dest_is_dir) { + rmdir $destpath + or syserr(g_('cannot remove destination directory %s'), $destpath); + } else { + $check_symlink->($destpath); + unlink $destpath + or syserr(g_('cannot remove destination file %s'), $destpath); + } + } + # If we are moving a directory, we do not need to walk it. + if ($src_is_dir) { + $File::Find::prune = 1; + } + rename $File::Find::name, $destpath + or syserr(g_('cannot move %s to %s'), $File::Find::name, $destpath); + }; + + find({ + wanted => $move_in_place, + no_chdir => 1, + dangling_symlinks => 0, + }, $tmp); } else { - rename($tmp, $dest) - or syserr(g_('unable to rename %s to %s'), $tmp, $dest); + # Rename extracted directory + opendir(my $dir_dh, $tmp) or syserr(g_('cannot opendir %s'), $tmp); + my @entries = grep { $_ ne '.' && $_ ne '..' } readdir($dir_dh); + closedir($dir_dh); + + erasedir($dest); + + if (scalar(@entries) == 1 && ! -l "$tmp/$entries[0]" && -d _) { + rename("$tmp/$entries[0]", $dest) + or syserr(g_('unable to rename %s to %s'), + "$tmp/$entries[0]", $dest); + } else { + rename($tmp, $dest) + or syserr(g_('unable to rename %s to %s'), $tmp, $dest); + } } erasedir($tmp); } diff -Nru dpkg-1.21.1ubuntu2/scripts/t/Dpkg_Source_Archive.t dpkg-1.21.1ubuntu2.1/scripts/t/Dpkg_Source_Archive.t --- dpkg-1.21.1ubuntu2/scripts/t/Dpkg_Source_Archive.t 2021-09-11 18:35:03.000000000 +0000 +++ dpkg-1.21.1ubuntu2.1/scripts/t/Dpkg_Source_Archive.t 2022-05-25 11:11:46.000000000 +0000 @@ -16,12 +16,120 @@ use strict; use warnings; -use Test::More tests => 1; +use Test::More tests => 4; +use Test::Dpkg qw(:paths); + +use File::Spec; +use File::Path qw(make_path rmtree); BEGIN { use_ok('Dpkg::Source::Archive'); } +use Dpkg; + +my $tmpdir = test_get_temp_path(); + +rmtree($tmpdir); + +sub test_touch +{ + my ($name, $data) = @_; + + open my $fh, '>', $name + or die "cannot touch file $name\n"; + print { $fh } $data if $data; + close $fh; +} + +sub test_path_escape +{ + my $name = shift; + + my $treedir = File::Spec->rel2abs("$tmpdir/$name-tree"); + my $overdir = File::Spec->rel2abs("$tmpdir/$name-overlay"); + my $outdir = "$tmpdir/$name-out"; + my $expdir = "$tmpdir/$name-exp"; + + # This is the base directory, where we are going to be extracting stuff + # into, which include traps. + make_path("$treedir/subdir-a"); + test_touch("$treedir/subdir-a/file-a"); + test_touch("$treedir/subdir-a/file-pre-a"); + make_path("$treedir/subdir-b"); + test_touch("$treedir/subdir-b/file-b"); + test_touch("$treedir/subdir-b/file-pre-b"); + symlink File::Spec->abs2rel($outdir, $treedir), "$treedir/symlink-escape"; + symlink File::Spec->abs2rel("$outdir/nonexistent", $treedir), "$treedir/symlink-nonexistent"; + symlink "$treedir/file", "$treedir/symlink-within"; + test_touch("$treedir/supposed-dir"); + + # This is the overlay directory, which we'll pack and extract over the + # base directory. + make_path($overdir); + make_path("$overdir/subdir-a/aa"); + test_touch("$overdir/subdir-a/aa/file-aa", 'aa'); + test_touch("$overdir/subdir-a/file-a", 'a'); + make_path("$overdir/subdir-b/bb"); + test_touch("$overdir/subdir-b/bb/file-bb", 'bb'); + test_touch("$overdir/subdir-b/file-b", 'b'); + make_path("$overdir/symlink-escape"); + test_touch("$overdir/symlink-escape/escaped-file", 'escaped'); + test_touch("$overdir/symlink-nonexistent", 'nonexistent'); + make_path("$overdir/symlink-within"); + make_path("$overdir/supposed-dir"); + test_touch("$overdir/supposed-dir/supposed-file", 'something'); + + # Generate overlay tar. + system($Dpkg::PROGTAR, '-cf', "$overdir.tar", '-C', $overdir, qw( + subdir-a subdir-b + symlink-escape/escaped-file symlink-nonexistent symlink-within + supposed-dir + )) == 0 + or die "cannot create overlay tar archive\n"; + + # This is the expected directory, which we'll be comparing against. + make_path($expdir); + system('cp', '-a', $overdir, $expdir) == 0 + or die "cannot copy overlay hierarchy into expected directory\n"; + + # Store the expected and out reference directories into a tar to compare + # its structure against the result reference. + system($Dpkg::PROGTAR, '-cf', "$expdir.tar", '-C', $overdir, qw( + subdir-a subdir-b + symlink-escape/escaped-file symlink-nonexistent symlink-within + supposed-dir + ), '-C', $treedir, qw( + subdir-a/file-pre-a + subdir-b/file-pre-b + )) == 0 + or die "cannot create expected tar archive\n"; + + # This directory is supposed to remain empty, anything inside implies a + # directory traversal. + make_path($outdir); + + my $warnseen; + local $SIG{__WARN__} = sub { $warnseen = $_[0] }; + + # Perform the extraction. + my $tar = Dpkg::Source::Archive->new(filename => "$overdir.tar"); + $tar->extract($treedir, in_place => 1); + + # Store the result into a tar to compare its structure against a reference. + system($Dpkg::PROGTAR, '-cf', "$treedir.tar", '-C', $treedir, '.'); + + # Check results + ok(length $warnseen && $warnseen =~ m/points outside source root/, + 'expected warning seen'); + ok(system($Dpkg::PROGTAR, '--compare', '-f', "$expdir.tar", '-C', $treedir) == 0, + 'expected directory matches'); + ok(! -e "$outdir/escaped-file", + 'expected output directory is empty, directory traversal'); +} + +test_path_escape('in-place'); + # TODO: Add actual test cases. 1; File /tmp/tmpxb72ncsp/vx5IswaboK/dpkg-1.21.1ubuntu2/tests/t-unpack-fifo/pkg-fifo/test-fifo is a fifo while file /tmp/tmpxb72ncsp/8_C1SkYR6p/dpkg-1.21.1ubuntu2.1/tests/t-unpack-fifo/pkg-fifo/test-fifo is a fifo File /tmp/tmpxb72ncsp/vx5IswaboK/dpkg-1.21.1ubuntu2/tests/t-unpack-hardlink/pkg-hardlink/test-fifo-link0 is a fifo while file /tmp/tmpxb72ncsp/8_C1SkYR6p/dpkg-1.21.1ubuntu2.1/tests/t-unpack-hardlink/pkg-hardlink/test-fifo-link0 is a fifo File /tmp/tmpxb72ncsp/vx5IswaboK/dpkg-1.21.1ubuntu2/tests/t-unpack-hardlink/pkg-hardlink/test-fifo-link1 is a fifo while file /tmp/tmpxb72ncsp/8_C1SkYR6p/dpkg-1.21.1ubuntu2.1/tests/t-unpack-hardlink/pkg-hardlink/test-fifo-link1 is a fifo