@@ -766,6 +766,87 @@ func TestUntarInvalidFilenames(t *testing.T) {
766766 }
767767}
768768
769+ // TestUntarParentTraversalContained verifies that entries whose names traverse
770+ // above the destination (including a bare "..") are rejected and never write
771+ // into the destination's parent. Regression test for the "write to the parent
772+ // of the extraction root" breakout.
773+ func TestUntarParentTraversalContained (t * testing.T ) {
774+ t .Skip ("FIXME: currently failing: enable once https://github.com/moby/go-archive/pull/45 is merged" )
775+ for _ , tc := range []struct {
776+ name string
777+ entry string
778+ }{
779+ {name : "bare parent" , entry : ".." },
780+ {name : "parent child" , entry : "../pwned" },
781+ {name : "nested parent" , entry : "../../pwned" },
782+ } {
783+ t .Run (tc .name , func (t * testing.T ) {
784+ base := t .TempDir ()
785+ dest := filepath .Join (base , "dest" )
786+ assert .NilError (t , os .Mkdir (dest , 0o755 ))
787+
788+ var buf bytes.Buffer
789+ tw := tar .NewWriter (& buf )
790+ assert .NilError (t , tw .WriteHeader (& tar.Header {
791+ Name : tc .entry ,
792+ Typeflag : tar .TypeReg ,
793+ Mode : 0o644 ,
794+ Size : int64 (len ("bad" )),
795+ }))
796+ _ , err := tw .Write ([]byte ("bad" ))
797+ assert .NilError (t , err )
798+ assert .NilError (t , tw .Close ())
799+
800+ // Untar must reject parent-traversal entries; regardless,
801+ // nothing may be written above dest.
802+ err = Untar (& buf , dest , & TarOptions {NoLchown : true })
803+ assert .ErrorType (t , err , & breakoutErr {})
804+
805+ // dest's parent must still contain only dest.
806+ entries , err := os .ReadDir (base )
807+ assert .NilError (t , err )
808+ assert .Equal (t , len (entries ), 1 , "unexpected escape into parent: %v" , entries )
809+ assert .Equal (t , entries [0 ].Name (), "dest" )
810+ })
811+ }
812+ }
813+
814+ // TestUntarSiblingPrefixContained verifies that a symlink whose target is a
815+ // sibling directory sharing the destination's path prefix (dest "base/dest",
816+ // sibling "base/dest-evil") cannot be written through. Regression test for the
817+ // old string-prefix (HasPrefix) containment check, which treated such a sibling
818+ // as inside the destination.
819+ func TestUntarSiblingPrefixContained (t * testing.T ) {
820+ t .Skip ("FIXME: currently failing: enable once https://github.com/moby/go-archive/pull/45 is merged" )
821+ base := t .TempDir ()
822+ dest := filepath .Join (base , "dest" )
823+ assert .NilError (t , os .Mkdir (dest , 0o755 ))
824+ // Prefix-sharing sibling with a sentinel file.
825+ evil := filepath .Join (base , "dest-evil" )
826+ assert .NilError (t , os .Mkdir (evil , 0o755 ))
827+
828+ assert .NilError (t , os .WriteFile (filepath .Join (evil , "secret" ), []byte ("secret" ), 0o600 ))
829+
830+ var buf bytes.Buffer
831+ tw := tar .NewWriter (& buf )
832+ // A hardlink whose target resolves to the prefix-sharing sibling. The old
833+ // strings.HasPrefix check accepted this because "base/dest-evil/secret"
834+ // starts with "base/dest".
835+ assert .NilError (t , tw .WriteHeader (& tar.Header {
836+ Name : "grab" ,
837+ Typeflag : tar .TypeLink ,
838+ Linkname : "../dest-evil/secret" ,
839+ Mode : 0o644 ,
840+ }))
841+ assert .NilError (t , tw .Close ())
842+
843+ _ = Untar (& buf , dest , & TarOptions {NoLchown : true }) // may error; we only require containment
844+
845+ // No hardlink to the sibling's secret may be created inside dest.
846+ _ , statErr := os .Stat (filepath .Join (dest , "grab" ))
847+ assert .ErrorIs (t , statErr , os .ErrNotExist , "hardlink to prefix-sibling created" )
848+ }
849+
769850func TestUntarHardlinkToSymlink (t * testing.T ) {
770851 skip .If (t , runtime .GOOS != "windows" && os .Getuid () != 0 , "skipping test that requires root" )
771852 for i , headers := range [][]* tar.Header {
0 commit comments