CVE-2018-1002207: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (CWE-22)
value: mholt/archiver golang package before e4ef56d48eb029648b0e895bb0b6a393ef0829c3 is vulnerable to directory traversal, allowing attackers to write to arbitrary files via a ../ (dot dot slash) in an archive entry that is mishandled during extraction. This vulnerability is also known as 'Zip-Slip'.
CWE description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
CVSS v3 base score: 5.5
Published: 2018-07-25T17:29Z
Repository: https://github.com/mholt/archiver
Language: Go
Files changed: archiver.go, cmd/archiver/main.go, rar.go, tar.go, zip.go
Commit date: 2018-04-17 16:02:35 -0600
Commit message: fix: prevent extraction of archived files outside target path (#65)

* fix: prevent extraction of archived files outside target path

* CR: consolidate the path sanitaiton logic

Diff: diff --git a/archiver.go b/archiver.go
index 68f8ea3..32baf3e 100644
--- a/archiver.go
+++ b/archiver.go
@@ -5,10 +5,11 @@ import (
 	"io"
 	"log"
 	"os"
 	"path/filepath"
 	"runtime"
+	"strings"
 )
 
 // Archiver represent a archive format
 type Archiver interface {
 	// Match checks supported files
@@ -103,5 +104,16 @@ func mkdir(dirPath string) error {
 	if err != nil {
 		return fmt.Errorf("%s: making directory: %v", dirPath, err)
 	}
 	return nil
 }
+
+func sanitizeExtractPath(filePath string, destination string) error {
+	// to avoid zip slip (writing outside of the destination), we resolve
+	// the target path, and make sure it's nested in the intended
+	// destination, or bail otherwise.
+	destpath := filepath.Join(destination, filePath)
+	if !strings.HasPrefix(destpath, destination) {
+		return fmt.Errorf("%s: illegal file path", filePath)
+	}
+	return nil
+}
diff --git a/cmd/archiver/main.go b/cmd/archiver/main.go
index d274e16..2e181fa 100644
--- a/cmd/archiver/main.go
+++ b/cmd/archiver/main.go
@@ -25,11 +25,14 @@ func main() {
 		if len(os.Args) < 4 {
 			fatal(usage)
 		}
 		err = ff.Make(filename, os.Args[3:])
 	case "open":
-		dest := ""
+		dest, osErr := os.Getwd()
+		if osErr != nil {
+			fatal(err)
+		}
 		if len(os.Args) == 4 {
 			dest = os.Args[3]
 		} else if len(os.Args) > 4 {
 			fatal(usage)
 		}
diff --git a/rar.go b/rar.go
index 7744c24..86f8a63 100644
--- a/rar.go
+++ b/rar.go
@@ -70,26 +70,33 @@ func (rarFormat) Read(input io.Reader, destination string) error {
 			break
 		} else if err != nil {
 			return err
 		}
 
+		err = sanitizeExtractPath(header.Name, destination)
+		if err != nil {
+			return err
+		}
+
+		destpath := filepath.Join(destination, header.Name)
+
 		if header.IsDir {
-			err = mkdir(filepath.Join(destination, header.Name))
+			err = mkdir(destpath)
 			if err != nil {
 				return err
 			}
 			continue
 		}
 
 		// if files come before their containing folders, then we must
 		// create their folders before writing the file
-		err = mkdir(filepath.Dir(filepath.Join(destination, header.Name)))
+		err = mkdir(filepath.Dir(destpath))
 		if err != nil {
 			return err
 		}
 
-		err = writeNewFile(filepath.Join(destination, header.Name), rr, header.Mode())
+		err = writeNewFile(destpath, rr, header.Mode())
 		if err != nil {
 			return err
 		}
 	}
 
diff --git a/tar.go b/tar.go
index 2fbfbb2..caa9de2 100644
--- a/tar.go
+++ b/tar.go
@@ -217,18 +217,25 @@ func untar(tr *tar.Reader, destination string) error {
 	return nil
 }
 
 // untarFile untars a single file from tr with header header into destination.
 func untarFile(tr *tar.Reader, header *tar.Header, destination string) error {
+	err := sanitizeExtractPath(header.Name, destination)
+	if err != nil {
+		return err
+	}
+
+	destpath := filepath.Join(destination, header.Name)
+
 	switch header.Typeflag {
 	case tar.TypeDir:
-		return mkdir(filepath.Join(destination, header.Name))
+		return mkdir(destpath)
 	case tar.TypeReg, tar.TypeRegA, tar.TypeChar, tar.TypeBlock, tar.TypeFifo:
-		return writeNewFile(filepath.Join(destination, header.Name), tr, header.FileInfo().Mode())
+		return writeNewFile(destpath, tr, header.FileInfo().Mode())
 	case tar.TypeSymlink:
-		return writeNewSymbolicLink(filepath.Join(destination, header.Name), header.Linkname)
+		return writeNewSymbolicLink(destpath, header.Linkname)
 	case tar.TypeLink:
-		return writeNewHardLink(filepath.Join(destination, header.Name), filepath.Join(destination, header.Linkname))
+		return writeNewHardLink(destpath, filepath.Join(destination, header.Linkname))
 	default:
 		return fmt.Errorf("%s: unknown type flag: %c", header.Name, header.Typeflag)
 	}
 }
diff --git a/zip.go b/zip.go
index 5f458ad..9d20bc1 100644
--- a/zip.go
+++ b/zip.go
@@ -185,10 +185,15 @@ func unzipAll(r *zip.Reader, destination string) error {
 
 	return nil
 }
 
 func unzipFile(zf *zip.File, destination string) error {
+	err := sanitizeExtractPath(zf.Name, destination)
+	if err != nil {
+		return err
+	}
+
 	if strings.HasSuffix(zf.Name, "/") {
 		return mkdir(filepath.Join(destination, zf.Name))
 	}
 
 	rc, err := zf.Open()

Vulnerable code: 		dest := ""
			err = mkdir(filepath.Join(destination, header.Name))
		err = mkdir(filepath.Dir(filepath.Join(destination, header.Name)))
		err = writeNewFile(filepath.Join(destination, header.Name), rr, header.Mode())
		return mkdir(filepath.Join(destination, header.Name))
		return writeNewFile(filepath.Join(destination, header.Name), tr, header.FileInfo().Mode())
		return writeNewSymbolicLink(filepath.Join(destination, header.Name), header.Linkname)
		return writeNewHardLink(filepath.Join(destination, header.Name), filepath.Join(destination, header.Linkname))
Fixed code: 	"strings"

func sanitizeExtractPath(filePath string, destination string) error {
	// to avoid zip slip (writing outside of the destination), we resolve
	// the target path, and make sure it's nested in the intended
	// destination, or bail otherwise.
	destpath := filepath.Join(destination, filePath)
	if !strings.HasPrefix(destpath, destination) {
		return fmt.Errorf("%s: illegal file path", filePath)
	}
	return nil
}
		dest, osErr := os.Getwd()
		if osErr != nil {
			fatal(err)
		}
		err = sanitizeExtractPath(header.Name, destination)
		if err != nil {
			return err
		}

		destpath := filepath.Join(destination, header.Name)

			err = mkdir(destpath)
		err = mkdir(filepath.Dir(destpath))
		err = writeNewFile(destpath, rr, header.Mode())
	err := sanitizeExtractPath(header.Name, destination)
	if err != nil {
		return err
	}

	destpath := filepath.Join(destination, header.Name)

		return mkdir(destpath)
		return writeNewFile(destpath, tr, header.FileInfo().Mode())
		return writeNewSymbolicLink(destpath, header.Linkname)
		return writeNewHardLink(destpath, filepath.Join(destination, header.Linkname))
	err := sanitizeExtractPath(zf.Name, destination)
	if err != nil {
		return err
	}