@@ -12,6 +12,7 @@ import (
1212 "net/http"
1313 "net/url"
1414 "os"
15+ "path"
1516 "path/filepath"
1617 "sort"
1718 "strings"
@@ -764,6 +765,12 @@ func extractTarGz(archivePath, destDir string) error {
764765 }
765766
766767 tr := tar .NewReader (reader )
768+ root , err := os .OpenRoot (destDir )
769+ if err != nil {
770+ return fmt .Errorf ("open extraction root: %w" , err )
771+ }
772+ defer root .Close ()
773+
767774 for {
768775 header , err := tr .Next ()
769776 if err == io .EOF {
@@ -773,41 +780,41 @@ func extractTarGz(archivePath, destDir string) error {
773780 return fmt .Errorf ("tar read: %w" , err )
774781 }
775782
776- cleanName := filepath . Clean ( header . Name )
777- target := filepath . Join ( destDir , cleanName )
778-
779- // Zip-slip guard: reject entries whose path escapes destDir (e.g .
780- // "../../etc/x"), so a malicious Feature tarball cannot write outside the
781- // extraction directory. filepath.Join cleans "..", so compare the result.
782- if target != destDir && ! strings . HasPrefix ( target , destDir + string ( os . PathSeparator ) ) {
783+ // Tar paths always use forward slashes. Localize rejects absolute paths,
784+ // parent traversal, and names that cannot be represented safely on the
785+ // current platform. Root also prevents escapes through symlinks already
786+ // present below destDir.
787+ cleanName := path . Clean ( header . Name )
788+ localName , err := filepath .Localize ( cleanName )
789+ if err != nil || ! filepath . IsLocal ( localName ) {
783790 return fmt .Errorf ("tar entry %q escapes the destination directory" , header .Name )
784791 }
785792
786793 switch header .Typeflag {
787794 case tar .TypeDir :
788- if err := os .MkdirAll (target , 0755 ); err != nil {
789- return err
795+ if err := root .MkdirAll (localName , 0755 ); err != nil {
796+ return fmt . Errorf ( "create directory for tar entry %q: %w" , header . Name , err )
790797 }
791798 case tar .TypeReg :
792- if err := os .MkdirAll (filepath .Dir (target ), 0755 ); err != nil {
793- return err
799+ if err := root .MkdirAll (filepath .Dir (localName ), 0755 ); err != nil {
800+ return fmt . Errorf ( "create parent for tar entry %q: %w" , header . Name , err )
794801 }
795- out , err := os .Create (target )
802+ out , err := root .Create (localName )
796803 if err != nil {
797- return err
804+ return fmt . Errorf ( "create tar entry %q: %w" , header . Name , err )
798805 }
799806 // A truncated copy must fail the install, not silently produce a
800807 // corrupt Feature that we report as success.
801808 if _ , err := io .Copy (out , tr ); err != nil {
802809 out .Close ()
803- return fmt .Errorf ("extract %s : %w" , target , err )
810+ return fmt .Errorf ("extract tar entry %q : %w" , header . Name , err )
804811 }
805812 if err := out .Close (); err != nil {
806- return fmt .Errorf ("close %s : %w" , target , err )
813+ return fmt .Errorf ("close tar entry %q : %w" , header . Name , err )
807814 }
808815 if header .Mode != 0 {
809- if err := os .Chmod (target , os .FileMode (header .Mode )); err != nil {
810- return err
816+ if err := root .Chmod (localName , os .FileMode (header .Mode )); err != nil {
817+ return fmt . Errorf ( "set mode on tar entry %q: %w" , header . Name , err )
811818 }
812819 }
813820 }
0 commit comments