Explorar el Código

Fix multiple security issues with dpkg-source (CVE-2010-1679)

- Enhance checks to catch maliciously crafted patches which could modify
  files outside of the unpacked source package.
- Do not consider a top-level symlink like a directory when
  extracting a tarball.
- Exclude .pc while extracting the upstream tarball in 3.0 (quilt)
  as patch blindly writes in that directory during unpack (and would
  follow any existing symlink).
Raphaël Hertzog hace 15 años
padre
commit
00aa1a8649

+ 11 - 1
debian/changelog

@@ -8,7 +8,17 @@ dpkg (1.15.8.8) UNRELEASED; urgency=low
   * Two typos fixed in French (Christian Perrier, thanks to Julien
   * Two typos fixed in French (Christian Perrier, thanks to Julien
     Valroff).
     Valroff).
 
 
- -- Christian Perrier <bubulle@debian.org>  Mon, 20 Dec 2010 07:12:32 +0100
+  [ Raphaël Hertzog ]
+  * Fix multiple security issues with dpkg-source (CVE-2010-1679):
+    - Enhance checks to catch maliciously crafted patches which could modify
+      files outside of the unpacked source package.
+    - Do not consider a top-level symlink like a directory when
+      extracting a tarball.
+    - Exclude .pc while extracting the upstream tarball in 3.0 (quilt)
+      as patch blindly writes in that directory during unpack (and would
+      follow any existing symlink).
+
+ -- Raphaël Hertzog <hertzog@debian.org>  Thu, 30 Dec 2010 17:31:56 +0100
 
 
 dpkg (1.15.8.7) unstable; urgency=low
 dpkg (1.15.8.7) unstable; urgency=low
 
 

+ 1 - 1
scripts/Dpkg/Source/Archive.pm

@@ -147,7 +147,7 @@ sub extract {
     closedir(D);
     closedir(D);
     my $done = 0;
     my $done = 0;
     erasedir($dest);
     erasedir($dest);
-    if (scalar(@entries) == 1 && -d "$tmp/$entries[0]") {
+    if (scalar(@entries) == 1 && ! -l "$tmp/$entries[0]" && -d _) {
 	rename("$tmp/$entries[0]", $dest) ||
 	rename("$tmp/$entries[0]", $dest) ||
 		syserr(_g("Unable to rename %s to %s"),
 		syserr(_g("Unable to rename %s to %s"),
 		       "$tmp/$entries[0]", $dest);
 		       "$tmp/$entries[0]", $dest);

+ 6 - 1
scripts/Dpkg/Source/Package/V2.pm

@@ -133,7 +133,12 @@ sub do_extract {
     # Extract main tarball
     # Extract main tarball
     info(_g("unpacking %s"), $tarfile);
     info(_g("unpacking %s"), $tarfile);
     my $tar = Dpkg::Source::Archive->new(filename => "$dscdir$tarfile");
     my $tar = Dpkg::Source::Archive->new(filename => "$dscdir$tarfile");
-    $tar->extract($newdirectory, no_fixperms => 1);
+    $tar->extract($newdirectory, no_fixperms => 1,
+                  options => [ "--anchored", "--no-wildcards-match-slash",
+                               "--exclude", "*/.pc", "--exclude", ".pc" ]);
+    # The .pc exclusion is only needed for 3.0 (quilt) and to avoid
+    # having an upstream tarball provide a directory with symlinks
+    # that would be blindly followed when applying the patches
 
 
     # Extract additional orig tarballs
     # Extract additional orig tarballs
     foreach my $subdir (keys %origtar) {
     foreach my $subdir (keys %origtar) {

+ 63 - 35
scripts/Dpkg/Source/Patch.pm

@@ -307,11 +307,41 @@ sub analyze {
         $header =~ s/\s.*// unless ($header =~ s/\t.*//);
         $header =~ s/\s.*// unless ($header =~ s/\t.*//);
         return $header;
         return $header;
     }
     }
+    sub intuit_file_patched {
+	my ($old, $new) = @_;
+	return $new unless defined $old;
+	return $old unless defined $new;
+	return $new if -e $new and not -e $old;
+	return $old if -e $old and not -e $new;
+	# We don't consider the case where both files are non-existent and
+	# where patch picks the one with the fewest directories to create
+	# since dpkg-source will pre-create the required directories
+	#
+	# Precalculate metrics used by patch
+	my ($tmp_o, $tmp_n) = ($old, $new);
+	my ($len_o, $len_n) = (length($old), length($new));
+	$tmp_o =~ s{[/\\]+}{/}g;
+	$tmp_n =~ s{[/\\]+}{/}g;
+	my $nb_comp_o = ($tmp_o =~ tr{/}{/});
+	my $nb_comp_n = ($tmp_n =~ tr{/}{/});
+	$tmp_o =~ s{^.*/}{};
+	$tmp_n =~ s{^.*/}{};
+	my ($blen_o, $blen_n) = (length($tmp_o), length($tmp_n));
+	# Decide like patch would
+	if ($nb_comp_o != $nb_comp_n) {
+	    return ($nb_comp_o < $nb_comp_n) ? $old : $new;
+	} elsif ($blen_o != $blen_n) {
+	    return ($blen_o < $blen_n) ? $old : $new;
+	} elsif ($len_o != $len_n) {
+	    return ($len_o < $len_n) ? $old : $new;
+	}
+	return $old;
+    }
     $_ = getline($self);
     $_ = getline($self);
 
 
   HUNK:
   HUNK:
     while (defined($_) || not eof($self)) {
     while (defined($_) || not eof($self)) {
-	my ($fn, $fn2);
+	my (%path, %fn);
 	# skip comments leading up to patch (if any)
 	# skip comments leading up to patch (if any)
 	until (/^--- /) {
 	until (/^--- /) {
 	    last HUNK if not defined($_ = getline($self));
 	    last HUNK if not defined($_ = getline($self));
@@ -321,11 +351,8 @@ sub analyze {
 	unless(s/^--- //) {
 	unless(s/^--- //) {
 	    error(_g("expected ^--- in line %d of diff `%s'"), $., $diff);
 	    error(_g("expected ^--- in line %d of diff `%s'"), $., $diff);
 	}
 	}
-        $_ = strip_ts($_);
-        if ($_ eq '/dev/null' or s{^[^/]+/}{$destdir/}) {
-            $fn = $_;
-	    error(_g("%s contains an insecure path: %s"), $diff, $_) if m{/\.\./};
-        }
+        $path{'old'} = $_ = strip_ts($_);
+	$fn{'old'} = $_ if $_ ne '/dev/null' and s{^[^/]*/+}{$destdir/};
 	if (/\.dpkg-orig$/) {
 	if (/\.dpkg-orig$/) {
 	    error(_g("diff `%s' patches file with name ending .dpkg-orig"), $diff);
 	    error(_g("diff `%s' patches file with name ending .dpkg-orig"), $diff);
 	}
 	}
@@ -336,46 +363,47 @@ sub analyze {
 	unless (s/^\+\+\+ //) {
 	unless (s/^\+\+\+ //) {
 	    error(_g("line after --- isn't as expected in diff `%s' (line %d)"), $diff, $.);
 	    error(_g("line after --- isn't as expected in diff `%s' (line %d)"), $diff, $.);
 	}
 	}
-        $_ = strip_ts($_);
-        if ($_ eq '/dev/null' or s{^[^/]+/}{$destdir/}) {
-            $fn2 = $_;
-	    error(_g("%s contains an insecure path: %s"), $diff, $_) if m{/\.\./};
-        } else {
-            unless (defined $fn) {
-                error(_g("none of the filenames in ---/+++ are relative in diff `%s' (line %d)"),
-                      $diff, $.);
-            }
-        }
+        $path{'new'} = $_ = strip_ts($_);
+	$fn{'new'} = $_ if $_ ne '/dev/null' and s{^[^/]*/+}{$destdir/};
+
+	unless (defined $fn{'old'} or defined $fn{'new'}) {
+	    error(_g("none of the filenames in ---/+++ are valid in diff '%s' (line %d)"),
+		  $diff, $.);
+	}
 
 
-        if (defined($fn) and $fn eq '/dev/null') {
+	# Safety checks on both filenames that patch could use
+	foreach my $key ("old", "new") {
+	    next unless defined $fn{$key};
+	    if ($path{$key} =~ m{/\.\./}) {
+		error(_g("%s contains an insecure path: %s"), $diff, $path{$key});
+	    }
+	    my $path = $fn{$key};
+	    while (1) {
+		if (-l $path) {
+		    error(_g("diff %s modifies file %s through a symlink: %s"),
+			  $diff, $fn{$key}, $path);
+		}
+		last unless $path =~ s{/+[^/]*$}{};
+		last if length($path) <= length($destdir); # $destdir is assumed safe
+	    }
+	}
+
+        if ($path{'old'} eq '/dev/null' and $path{'new'} eq '/dev/null') {
             error(_g("original and modified files are /dev/null in diff `%s' (line %d)"),
             error(_g("original and modified files are /dev/null in diff `%s' (line %d)"),
-                  $diff, $.) if (defined($fn2) and $fn2 eq '/dev/null');
-            $fn = $fn2;
-        } elsif (defined($fn2) and $fn2 ne '/dev/null') {
-            $fn = $fn2 unless defined $fn;
-            $fn = $fn2 if ((not -e $fn) and -e $fn2);
-        } elsif (defined($fn2) and $fn2 eq '/dev/null') {
+                  $diff, $.);
+        } elsif ($path{'new'} eq '/dev/null') {
             error(_g("file removal without proper filename in diff `%s' (line %d)"),
             error(_g("file removal without proper filename in diff `%s' (line %d)"),
-                  $diff, $. - 1) unless defined $fn;
+                  $diff, $. - 1) unless defined $fn{'old'};
             warning(_g("diff %s removes a non-existing file %s (line %d)"),
             warning(_g("diff %s removes a non-existing file %s (line %d)"),
-                    $diff, $fn, $.) unless -e $fn;
+                    $diff, $fn{'old'}, $.) unless -e $fn{'old'};
         }
         }
+	my $fn = intuit_file_patched($fn{'old'}, $fn{'new'});
 
 
 	my $dirname = $fn;
 	my $dirname = $fn;
 	if ($dirname =~ s{/[^/]+$}{} && not -d $dirname) {
 	if ($dirname =~ s{/[^/]+$}{} && not -d $dirname) {
 	    $dirtocreate{$dirname} = 1;
 	    $dirtocreate{$dirname} = 1;
 	}
 	}
 
 
-	# Sanity check, refuse to patch through a symlink
-	$dirname = $fn;
-	while (1) {
-	    if (-l $dirname) {
-		error(_g("diff %s modifies file %s through a symlink: %s"),
-		      $diff, $fn, $dirname);
-	    }
-	    last unless $dirname =~ s{/[^/]+$}{};
-	}
-
 	if (-e $fn and not -f _) {
 	if (-e $fn and not -f _) {
 	    error(_g("diff `%s' patches something which is not a plain file"), $diff);
 	    error(_g("diff `%s' patches something which is not a plain file"), $diff);
 	}
 	}