diff --git a/tar.go b/tar.go index be898665..45be6ca0 100644 --- a/tar.go +++ b/tar.go @@ -238,6 +238,18 @@ func (t *Tar) untarNext(destination string) error { return fmt.Errorf("checking path traversal attempt: %v", errPath) } + switch header.Typeflag { + case tar.TypeSymlink, tar.TypeLink: + // though we've already checked the name for possible path traversals, it is possible + // to write content though a symlink to a path outside of the destination folder + // with multiple header entries. We should consider any symlink or hardlink that points + // to outside of the destination folder to be a possible path traversal attack. + errPath = t.CheckPath(destination, header.Linkname) + if errPath != nil { + return fmt.Errorf("checking path traversal attempt in symlink: %v", errPath) + } + } + if t.StripComponents > 0 { if strings.Count(header.Name, "/") < t.StripComponents { return nil // skip path with fewer components diff --git a/tar_test.go b/tar_test.go index 7a9d3541..7ad43d29 100644 --- a/tar_test.go +++ b/tar_test.go @@ -1,14 +1,24 @@ package archiver_test import ( + "archive/tar" + "bytes" "io/ioutil" "os" "path" + "path/filepath" "testing" "github.com/mholt/archiver/v3" ) +func requireDoesNotExist(t *testing.T, path string) { + _, err := os.Stat(path) + if err == nil { + t.Fatalf("'%s' expected to not exist", path) + } +} + func requireRegularFile(t *testing.T, path string) os.FileInfo { fileInfo, err := os.Stat(path) if err != nil { @@ -47,6 +57,68 @@ func TestDefaultTar_Unarchive_HardlinkSuccess(t *testing.T) { assertSameFile(t, fileaInfo, filebInfo) } +func TestDefaultTar_Unarchive_SymlinkPathTraversal(t *testing.T) { + tmp := t.TempDir() + source := filepath.Join(tmp, "source.tar") + createSymlinkPathTraversalSample(t, source) + destination := filepath.Join(tmp, "destination") + + err := archiver.DefaultTar.Unarchive(source, destination) + if err != nil { + t.Fatalf("unarchiving '%s' to '%s': %v", source, destination, err) + } + + requireDoesNotExist(t, filepath.Join(tmp, "target")) + requireRegularFile(t, filepath.Join(tmp, "destination", "symlinkvehicle.txt")) +} + +func createSymlinkPathTraversalSample(t *testing.T, archivePath string) { + t.Helper() + + type tarinfo struct { + Name string + Link string + Body string + Type byte + } + + var infos = []tarinfo{ + {"symlinkvehicle.txt", "./../target", "", tar.TypeSymlink}, + {"symlinkvehicle.txt", "", "content modified!", tar.TypeReg}, + } + + var buf bytes.Buffer + var tw = tar.NewWriter(&buf) + for _, ti := range infos { + hdr := &tar.Header{ + Name: ti.Name, + Mode: 0600, + Linkname: ti.Link, + Typeflag: ti.Type, + Size: int64(len(ti.Body)), + } + if err := tw.WriteHeader(hdr); err != nil { + t.Fatal("Writing header: ", err) + } + if _, err := tw.Write([]byte(ti.Body)); err != nil { + t.Fatal("Writing body: ", err) + } + } + + f, err := os.Create(archivePath) + if err != nil { + t.Fatal(err) + } + _, err = f.Write(buf.Bytes()) + if err != nil { + t.Fatal(err) + } + + if err := f.Close(); err != nil { + t.Fatal(err) + } +} + func TestDefaultTar_Extract_HardlinkSuccess(t *testing.T) { source := "testdata/gnu-hardlinks.tar"