diff -Nru dpkg-1.19.7ubuntu3/debian/changelog dpkg-1.19.7ubuntu3.2/debian/changelog --- dpkg-1.19.7ubuntu3/debian/changelog 2020-03-23 06:12:54.000000000 +0000 +++ dpkg-1.19.7ubuntu3.2/debian/changelog 2022-05-25 11:14:20.000000000 +0000 @@ -1,3 +1,21 @@ +dpkg (1.19.7ubuntu3.2) focal-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:14:20 -0400 + +dpkg (1.19.7ubuntu3.1) focal; urgency=medium + + [ William 'jawn-smith' Wilson ] + * scripts/Dpkg/Vendor/Ubuntu.pm: When checking for the correct + maintainer field, also look for canonical.com email addresses + (LP: #1951988) + + -- Brian Murray Wed, 15 Dec 2021 12:02:54 -0800 + dpkg (1.19.7ubuntu3) focal; urgency=medium * No-change rebuild for libgcc-s1 package name change. diff -Nru dpkg-1.19.7ubuntu3/scripts/Dpkg/Source/Archive.pm dpkg-1.19.7ubuntu3.2/scripts/Dpkg/Source/Archive.pm --- dpkg-1.19.7ubuntu3/scripts/Dpkg/Source/Archive.pm 2019-09-05 21:05:14.000000000 +0000 +++ dpkg-1.19.7ubuntu3.2/scripts/Dpkg/Source/Archive.pm 2022-05-25 11:14:17.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.19.7ubuntu3/scripts/Dpkg/Vendor/Ubuntu.pm dpkg-1.19.7ubuntu3.2/scripts/Dpkg/Vendor/Ubuntu.pm --- dpkg-1.19.7ubuntu3/scripts/Dpkg/Vendor/Ubuntu.pm 2019-09-05 21:05:14.000000000 +0000 +++ dpkg-1.19.7ubuntu3.2/scripts/Dpkg/Vendor/Ubuntu.pm 2021-12-15 20:02:52.000000000 +0000 @@ -54,8 +54,8 @@ # https://wiki.ubuntu.com/DebianMaintainerField if (defined($fields->{'Version'}) and defined($fields->{'Maintainer'}) and $fields->{'Version'} =~ /ubuntu/) { - if ($fields->{'Maintainer'} !~ /ubuntu/i) { - if (length $ENV{DEBEMAIL} and $ENV{DEBEMAIL} =~ /\@ubuntu\.com/) { + if ($fields->{'Maintainer'} !~ /(?:ubuntu|canonical)/i) { + if (length $ENV{DEBEMAIL} and $ENV{DEBEMAIL} =~ /\@(?:ubuntu|canonical)\.com/) { error(g_('Version number suggests Ubuntu changes, but Maintainer: does not have Ubuntu address')); } else { warning(g_('Version number suggests Ubuntu changes, but Maintainer: does not have Ubuntu address')); diff -Nru dpkg-1.19.7ubuntu3/scripts/t/Dpkg_Source_Archive.t dpkg-1.19.7ubuntu3.2/scripts/t/Dpkg_Source_Archive.t --- dpkg-1.19.7ubuntu3/scripts/t/Dpkg_Source_Archive.t 2019-08-24 17:25:11.000000000 +0000 +++ dpkg-1.19.7ubuntu3.2/scripts/t/Dpkg_Source_Archive.t 2022-05-25 11:14:17.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;