diff options
| author | Sakib Sajal <sakib.sajal@windriver.com> | 2022-07-26 00:33:07 -0400 |
|---|---|---|
| committer | Richard Purdie <richard.purdie@linuxfoundation.org> | 2022-08-01 16:27:29 +0100 |
| commit | 892bc9abc52272a0dd2f2877ea8a41fbc619ce9d (patch) | |
| tree | e281b6c3f5d7f2907ef205188c5e64dd928f791f | |
| parent | 7ac005ba752e10126a7ca75e386968f3d627e54b (diff) | |
| download | poky-892bc9abc52272a0dd2f2877ea8a41fbc619ce9d.tar.gz | |
dpkg: fix CVE-2022-1664
Backport patch to fix CVE-2022-1664.
(From OE-Core rev: e22b7b2a30c4c688b9c4414824c93ae8a884496c)
Signed-off-by: Sakib Sajal <sakib.sajal@windriver.com>
Signed-off-by: Steve Sakoman <steve@sakoman.com>
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
| -rw-r--r-- | meta/recipes-devtools/dpkg/dpkg/0001-Dpkg-Source-Archive-Prevent-directory-traversal-for-.patch | 328 | ||||
| -rw-r--r-- | meta/recipes-devtools/dpkg/dpkg_1.21.4.bb | 1 |
2 files changed, 329 insertions, 0 deletions
diff --git a/meta/recipes-devtools/dpkg/dpkg/0001-Dpkg-Source-Archive-Prevent-directory-traversal-for-.patch b/meta/recipes-devtools/dpkg/dpkg/0001-Dpkg-Source-Archive-Prevent-directory-traversal-for-.patch new file mode 100644 index 0000000000..d249d854fb --- /dev/null +++ b/meta/recipes-devtools/dpkg/dpkg/0001-Dpkg-Source-Archive-Prevent-directory-traversal-for-.patch | |||
| @@ -0,0 +1,328 @@ | |||
| 1 | From 6d8a6799639f8853a2af1f9036bc70fddbfdd2a2 Mon Sep 17 00:00:00 2001 | ||
| 2 | From: Guillem Jover <guillem@debian.org> | ||
| 3 | Date: Tue, 3 May 2022 02:09:32 +0200 | ||
| 4 | Subject: [PATCH] Dpkg::Source::Archive: Prevent directory traversal for | ||
| 5 | in-place extracts | ||
| 6 | |||
| 7 | For untrusted v2 and v3 source package formats that include a debian.tar | ||
| 8 | archive, when we are extracting it, we do that as an in-place extraction, | ||
| 9 | which can lead to directory traversal situations on specially crafted | ||
| 10 | orig.tar and debian.tar tarballs. | ||
| 11 | |||
| 12 | GNU tar replaces entries on the filesystem by the entries present on | ||
| 13 | the tarball, but it will follow symlinks when the symlink pathname | ||
| 14 | itself is not present as an actual directory on the tarball. | ||
| 15 | |||
| 16 | This means we can create an orig.tar where there's a symlink pointing | ||
| 17 | out of the source tree root directory, and then a debian.tar that | ||
| 18 | contains an entry within that symlink as if it was a directory, without | ||
| 19 | a directory entry for the symlink pathname itself, which will be | ||
| 20 | extracted following the symlink outside the source tree root. | ||
| 21 | |||
| 22 | This is currently noted as expected in GNU tar documentation. But even | ||
| 23 | if there was a new extraction mode avoiding this problem we'd need such | ||
| 24 | new version. Using perl's Archive::Tar would solve the problem, but | ||
| 25 | switching to such different pure perl implementation, could cause | ||
| 26 | compatibility or performance issues. | ||
| 27 | |||
| 28 | What we do is when we are requested to perform an in-place extract, we | ||
| 29 | instead still use a temporary directory, then walk that directory and | ||
| 30 | remove any matching entry in the destination directory, replicating what | ||
| 31 | GNU tar would do, but in addition avoiding the directory traversal issue | ||
| 32 | for symlinks. Which should work with any tar implementation and be safe. | ||
| 33 | |||
| 34 | Reported-by: Max Justicz <max@justi.cz> | ||
| 35 | Stable-Candidates: 1.18.x 1.19.x 1.20.x | ||
| 36 | Fixes: commit 0c0057a27fecccab77d2b3cffa9a7d172846f0b4 (1.14.17) | ||
| 37 | Fixes: CVE-2022-1664 | ||
| 38 | |||
| 39 | CVE: CVE-2022-1664 | ||
| 40 | Upstream-Status: Backport [7a6c03cb34d4a09f35df2f10779cbf1b70a5200b] | ||
| 41 | |||
| 42 | Signed-off-by: Sakib Sajal <sakib.sajal@windriver.com> | ||
| 43 | --- | ||
| 44 | scripts/Dpkg/Source/Archive.pm | 122 +++++++++++++++++++++++++------- | ||
| 45 | scripts/t/Dpkg_Source_Archive.t | 110 +++++++++++++++++++++++++++- | ||
| 46 | 2 files changed, 204 insertions(+), 28 deletions(-) | ||
| 47 | |||
| 48 | diff --git a/scripts/Dpkg/Source/Archive.pm b/scripts/Dpkg/Source/Archive.pm | ||
| 49 | index 33c181b20..2ddd04af8 100644 | ||
| 50 | --- a/scripts/Dpkg/Source/Archive.pm | ||
| 51 | +++ b/scripts/Dpkg/Source/Archive.pm | ||
| 52 | @@ -21,9 +21,11 @@ use warnings; | ||
| 53 | our $VERSION = '0.01'; | ||
| 54 | |||
| 55 | use Carp; | ||
| 56 | +use Errno qw(ENOENT); | ||
| 57 | use File::Temp qw(tempdir); | ||
| 58 | use File::Basename qw(basename); | ||
| 59 | use File::Spec; | ||
| 60 | +use File::Find; | ||
| 61 | use Cwd; | ||
| 62 | |||
| 63 | use Dpkg (); | ||
| 64 | @@ -110,19 +112,13 @@ sub extract { | ||
| 65 | my %spawn_opts = (wait_child => 1); | ||
| 66 | |||
| 67 | # Prepare destination | ||
| 68 | - my $tmp; | ||
| 69 | - if ($opts{in_place}) { | ||
| 70 | - $spawn_opts{chdir} = $dest; | ||
| 71 | - $tmp = $dest; # So that fixperms call works | ||
| 72 | - } else { | ||
| 73 | - my $template = basename($self->get_filename()) . '.tmp-extract.XXXXX'; | ||
| 74 | - unless (-e $dest) { | ||
| 75 | - # Kludge so that realpath works | ||
| 76 | - mkdir($dest) or syserr(g_('cannot create directory %s'), $dest); | ||
| 77 | - } | ||
| 78 | - $tmp = tempdir($template, DIR => Cwd::realpath("$dest/.."), CLEANUP => 1); | ||
| 79 | - $spawn_opts{chdir} = $tmp; | ||
| 80 | + my $template = basename($self->get_filename()) . '.tmp-extract.XXXXX'; | ||
| 81 | + unless (-e $dest) { | ||
| 82 | + # Kludge so that realpath works | ||
| 83 | + mkdir($dest) or syserr(g_('cannot create directory %s'), $dest); | ||
| 84 | } | ||
| 85 | + my $tmp = tempdir($template, DIR => Cwd::realpath("$dest/.."), CLEANUP => 1); | ||
| 86 | + $spawn_opts{chdir} = $tmp; | ||
| 87 | |||
| 88 | # Prepare stuff that handles the input of tar | ||
| 89 | $self->ensure_open('r', delete_sig => [ 'PIPE' ]); | ||
| 90 | @@ -145,22 +141,94 @@ sub extract { | ||
| 91 | # have to be calculated using mount options and other madness. | ||
| 92 | fixperms($tmp) unless $opts{no_fixperms}; | ||
| 93 | |||
| 94 | - # Stop here if we extracted in-place as there's nothing to move around | ||
| 95 | - return if $opts{in_place}; | ||
| 96 | - | ||
| 97 | - # Rename extracted directory | ||
| 98 | - opendir(my $dir_dh, $tmp) or syserr(g_('cannot opendir %s'), $tmp); | ||
| 99 | - my @entries = grep { $_ ne '.' && $_ ne '..' } readdir($dir_dh); | ||
| 100 | - closedir($dir_dh); | ||
| 101 | - my $done = 0; | ||
| 102 | - erasedir($dest); | ||
| 103 | - if (scalar(@entries) == 1 && ! -l "$tmp/$entries[0]" && -d _) { | ||
| 104 | - rename("$tmp/$entries[0]", $dest) | ||
| 105 | - or syserr(g_('unable to rename %s to %s'), | ||
| 106 | - "$tmp/$entries[0]", $dest); | ||
| 107 | + # If we are extracting "in-place" do not remove the destination directory. | ||
| 108 | + if ($opts{in_place}) { | ||
| 109 | + my $canon_basedir = Cwd::realpath($dest); | ||
| 110 | + # On Solaris /dev/null points to /devices/pseudo/mm@0:null. | ||
| 111 | + my $canon_devnull = Cwd::realpath('/dev/null'); | ||
| 112 | + my $check_symlink = sub { | ||
| 113 | + my $pathname = shift; | ||
| 114 | + my $canon_pathname = Cwd::realpath($pathname); | ||
| 115 | + if (not defined $canon_pathname) { | ||
| 116 | + return if $! == ENOENT; | ||
| 117 | + | ||
| 118 | + syserr(g_("pathname '%s' cannot be canonicalized"), $pathname); | ||
| 119 | + } | ||
| 120 | + return if $canon_pathname eq $canon_devnull; | ||
| 121 | + return if $canon_pathname eq $canon_basedir; | ||
| 122 | + return if $canon_pathname =~ m{^\Q$canon_basedir/\E}; | ||
| 123 | + warning(g_("pathname '%s' points outside source root (to '%s')"), | ||
| 124 | + $pathname, $canon_pathname); | ||
| 125 | + }; | ||
| 126 | + | ||
| 127 | + my $move_in_place = sub { | ||
| 128 | + my $relpath = File::Spec->abs2rel($File::Find::name, $tmp); | ||
| 129 | + my $destpath = File::Spec->catfile($dest, $relpath); | ||
| 130 | + | ||
| 131 | + my ($mode, $atime, $mtime); | ||
| 132 | + lstat $File::Find::name | ||
| 133 | + or syserr(g_('cannot get source pathname %s metadata'), $File::Find::name); | ||
| 134 | + ((undef) x 2, $mode, (undef) x 5, $atime, $mtime) = lstat _; | ||
| 135 | + my $src_is_dir = -d _; | ||
| 136 | + | ||
| 137 | + my $dest_exists = 1; | ||
| 138 | + if (not lstat $destpath) { | ||
| 139 | + if ($! == ENOENT) { | ||
| 140 | + $dest_exists = 0; | ||
| 141 | + } else { | ||
| 142 | + syserr(g_('cannot get target pathname %s metadata'), $destpath); | ||
| 143 | + } | ||
| 144 | + } | ||
| 145 | + my $dest_is_dir = -d _; | ||
| 146 | + if ($dest_exists) { | ||
| 147 | + if ($dest_is_dir && $src_is_dir) { | ||
| 148 | + # Refresh the destination directory attributes with the | ||
| 149 | + # ones from the tarball. | ||
| 150 | + chmod $mode, $destpath | ||
| 151 | + or syserr(g_('cannot change directory %s mode'), $File::Find::name); | ||
| 152 | + utime $atime, $mtime, $destpath | ||
| 153 | + or syserr(g_('cannot change directory %s times'), $File::Find::name); | ||
| 154 | + | ||
| 155 | + # We should do nothing, and just walk further tree. | ||
| 156 | + return; | ||
| 157 | + } elsif ($dest_is_dir) { | ||
| 158 | + rmdir $destpath | ||
| 159 | + or syserr(g_('cannot remove destination directory %s'), $destpath); | ||
| 160 | + } else { | ||
| 161 | + $check_symlink->($destpath); | ||
| 162 | + unlink $destpath | ||
| 163 | + or syserr(g_('cannot remove destination file %s'), $destpath); | ||
| 164 | + } | ||
| 165 | + } | ||
| 166 | + # If we are moving a directory, we do not need to walk it. | ||
| 167 | + if ($src_is_dir) { | ||
| 168 | + $File::Find::prune = 1; | ||
| 169 | + } | ||
| 170 | + rename $File::Find::name, $destpath | ||
| 171 | + or syserr(g_('cannot move %s to %s'), $File::Find::name, $destpath); | ||
| 172 | + }; | ||
| 173 | + | ||
| 174 | + find({ | ||
| 175 | + wanted => $move_in_place, | ||
| 176 | + no_chdir => 1, | ||
| 177 | + dangling_symlinks => 0, | ||
| 178 | + }, $tmp); | ||
| 179 | } else { | ||
| 180 | - rename($tmp, $dest) | ||
| 181 | - or syserr(g_('unable to rename %s to %s'), $tmp, $dest); | ||
| 182 | + # Rename extracted directory | ||
| 183 | + opendir(my $dir_dh, $tmp) or syserr(g_('cannot opendir %s'), $tmp); | ||
| 184 | + my @entries = grep { $_ ne '.' && $_ ne '..' } readdir($dir_dh); | ||
| 185 | + closedir($dir_dh); | ||
| 186 | + | ||
| 187 | + erasedir($dest); | ||
| 188 | + | ||
| 189 | + if (scalar(@entries) == 1 && ! -l "$tmp/$entries[0]" && -d _) { | ||
| 190 | + rename("$tmp/$entries[0]", $dest) | ||
| 191 | + or syserr(g_('unable to rename %s to %s'), | ||
| 192 | + "$tmp/$entries[0]", $dest); | ||
| 193 | + } else { | ||
| 194 | + rename($tmp, $dest) | ||
| 195 | + or syserr(g_('unable to rename %s to %s'), $tmp, $dest); | ||
| 196 | + } | ||
| 197 | } | ||
| 198 | erasedir($tmp); | ||
| 199 | } | ||
| 200 | diff --git a/scripts/t/Dpkg_Source_Archive.t b/scripts/t/Dpkg_Source_Archive.t | ||
| 201 | index 7b70da68e..504fbe1d4 100644 | ||
| 202 | --- a/scripts/t/Dpkg_Source_Archive.t | ||
| 203 | +++ b/scripts/t/Dpkg_Source_Archive.t | ||
| 204 | @@ -16,12 +16,120 @@ | ||
| 205 | use strict; | ||
| 206 | use warnings; | ||
| 207 | |||
| 208 | -use Test::More tests => 1; | ||
| 209 | +use Test::More tests => 4; | ||
| 210 | +use Test::Dpkg qw(:paths); | ||
| 211 | + | ||
| 212 | +use File::Spec; | ||
| 213 | +use File::Path qw(make_path rmtree); | ||
| 214 | |||
| 215 | BEGIN { | ||
| 216 | use_ok('Dpkg::Source::Archive'); | ||
| 217 | } | ||
| 218 | |||
| 219 | +use Dpkg; | ||
| 220 | + | ||
| 221 | +my $tmpdir = test_get_temp_path(); | ||
| 222 | + | ||
| 223 | +rmtree($tmpdir); | ||
| 224 | + | ||
| 225 | +sub test_touch | ||
| 226 | +{ | ||
| 227 | + my ($name, $data) = @_; | ||
| 228 | + | ||
| 229 | + open my $fh, '>', $name | ||
| 230 | + or die "cannot touch file $name\n"; | ||
| 231 | + print { $fh } $data if $data; | ||
| 232 | + close $fh; | ||
| 233 | +} | ||
| 234 | + | ||
| 235 | +sub test_path_escape | ||
| 236 | +{ | ||
| 237 | + my $name = shift; | ||
| 238 | + | ||
| 239 | + my $treedir = File::Spec->rel2abs("$tmpdir/$name-tree"); | ||
| 240 | + my $overdir = File::Spec->rel2abs("$tmpdir/$name-overlay"); | ||
| 241 | + my $outdir = "$tmpdir/$name-out"; | ||
| 242 | + my $expdir = "$tmpdir/$name-exp"; | ||
| 243 | + | ||
| 244 | + # This is the base directory, where we are going to be extracting stuff | ||
| 245 | + # into, which include traps. | ||
| 246 | + make_path("$treedir/subdir-a"); | ||
| 247 | + test_touch("$treedir/subdir-a/file-a"); | ||
| 248 | + test_touch("$treedir/subdir-a/file-pre-a"); | ||
| 249 | + make_path("$treedir/subdir-b"); | ||
| 250 | + test_touch("$treedir/subdir-b/file-b"); | ||
| 251 | + test_touch("$treedir/subdir-b/file-pre-b"); | ||
| 252 | + symlink File::Spec->abs2rel($outdir, $treedir), "$treedir/symlink-escape"; | ||
| 253 | + symlink File::Spec->abs2rel("$outdir/nonexistent", $treedir), "$treedir/symlink-nonexistent"; | ||
| 254 | + symlink "$treedir/file", "$treedir/symlink-within"; | ||
| 255 | + test_touch("$treedir/supposed-dir"); | ||
| 256 | + | ||
| 257 | + # This is the overlay directory, which we'll pack and extract over the | ||
| 258 | + # base directory. | ||
| 259 | + make_path($overdir); | ||
| 260 | + make_path("$overdir/subdir-a/aa"); | ||
| 261 | + test_touch("$overdir/subdir-a/aa/file-aa", 'aa'); | ||
| 262 | + test_touch("$overdir/subdir-a/file-a", 'a'); | ||
| 263 | + make_path("$overdir/subdir-b/bb"); | ||
| 264 | + test_touch("$overdir/subdir-b/bb/file-bb", 'bb'); | ||
| 265 | + test_touch("$overdir/subdir-b/file-b", 'b'); | ||
| 266 | + make_path("$overdir/symlink-escape"); | ||
| 267 | + test_touch("$overdir/symlink-escape/escaped-file", 'escaped'); | ||
| 268 | + test_touch("$overdir/symlink-nonexistent", 'nonexistent'); | ||
| 269 | + make_path("$overdir/symlink-within"); | ||
| 270 | + make_path("$overdir/supposed-dir"); | ||
| 271 | + test_touch("$overdir/supposed-dir/supposed-file", 'something'); | ||
| 272 | + | ||
| 273 | + # Generate overlay tar. | ||
| 274 | + system($Dpkg::PROGTAR, '-cf', "$overdir.tar", '-C', $overdir, qw( | ||
| 275 | + subdir-a subdir-b | ||
| 276 | + symlink-escape/escaped-file symlink-nonexistent symlink-within | ||
| 277 | + supposed-dir | ||
| 278 | + )) == 0 | ||
| 279 | + or die "cannot create overlay tar archive\n"; | ||
| 280 | + | ||
| 281 | + # This is the expected directory, which we'll be comparing against. | ||
| 282 | + make_path($expdir); | ||
| 283 | + system('cp', '-a', $overdir, $expdir) == 0 | ||
| 284 | + or die "cannot copy overlay hierarchy into expected directory\n"; | ||
| 285 | + | ||
| 286 | + # Store the expected and out reference directories into a tar to compare | ||
| 287 | + # its structure against the result reference. | ||
| 288 | + system($Dpkg::PROGTAR, '-cf', "$expdir.tar", '-C', $overdir, qw( | ||
| 289 | + subdir-a subdir-b | ||
| 290 | + symlink-escape/escaped-file symlink-nonexistent symlink-within | ||
| 291 | + supposed-dir | ||
| 292 | + ), '-C', $treedir, qw( | ||
| 293 | + subdir-a/file-pre-a | ||
| 294 | + subdir-b/file-pre-b | ||
| 295 | + )) == 0 | ||
| 296 | + or die "cannot create expected tar archive\n"; | ||
| 297 | + | ||
| 298 | + # This directory is supposed to remain empty, anything inside implies a | ||
| 299 | + # directory traversal. | ||
| 300 | + make_path($outdir); | ||
| 301 | + | ||
| 302 | + my $warnseen; | ||
| 303 | + local $SIG{__WARN__} = sub { $warnseen = $_[0] }; | ||
| 304 | + | ||
| 305 | + # Perform the extraction. | ||
| 306 | + my $tar = Dpkg::Source::Archive->new(filename => "$overdir.tar"); | ||
| 307 | + $tar->extract($treedir, in_place => 1); | ||
| 308 | + | ||
| 309 | + # Store the result into a tar to compare its structure against a reference. | ||
| 310 | + system($Dpkg::PROGTAR, '-cf', "$treedir.tar", '-C', $treedir, '.'); | ||
| 311 | + | ||
| 312 | + # Check results | ||
| 313 | + ok(length $warnseen && $warnseen =~ m/points outside source root/, | ||
| 314 | + 'expected warning seen'); | ||
| 315 | + ok(system($Dpkg::PROGTAR, '--compare', '-f', "$expdir.tar", '-C', $treedir) == 0, | ||
| 316 | + 'expected directory matches'); | ||
| 317 | + ok(! -e "$outdir/escaped-file", | ||
| 318 | + 'expected output directory is empty, directory traversal'); | ||
| 319 | +} | ||
| 320 | + | ||
| 321 | +test_path_escape('in-place'); | ||
| 322 | + | ||
| 323 | # TODO: Add actual test cases. | ||
| 324 | |||
| 325 | 1; | ||
| 326 | -- | ||
| 327 | 2.33.0 | ||
| 328 | |||
diff --git a/meta/recipes-devtools/dpkg/dpkg_1.21.4.bb b/meta/recipes-devtools/dpkg/dpkg_1.21.4.bb index 681909f0bf..7ef6233ee4 100644 --- a/meta/recipes-devtools/dpkg/dpkg_1.21.4.bb +++ b/meta/recipes-devtools/dpkg/dpkg_1.21.4.bb | |||
| @@ -14,6 +14,7 @@ SRC_URI = "git://salsa.debian.org/dpkg-team/dpkg.git;protocol=https;branch=main | |||
| 14 | file://0001-dpkg-Support-muslx32-build.patch \ | 14 | file://0001-dpkg-Support-muslx32-build.patch \ |
| 15 | file://pager.patch \ | 15 | file://pager.patch \ |
| 16 | file://0001-Add-support-for-riscv32-CPU.patch \ | 16 | file://0001-Add-support-for-riscv32-CPU.patch \ |
| 17 | file://0001-Dpkg-Source-Archive-Prevent-directory-traversal-for-.patch \ | ||
| 17 | " | 18 | " |
| 18 | 19 | ||
| 19 | SRC_URI:append:class-native = " file://0001-build.c-ignore-return-of-1-from-tar-cf.patch" | 20 | SRC_URI:append:class-native = " file://0001-build.c-ignore-return-of-1-from-tar-cf.patch" |
