Skip to content

Commit

Permalink
Inspect archive before extraction (#433)
Browse files Browse the repository at this point in the history
  • Loading branch information
yahavi authored Oct 15, 2021
1 parent 4505850 commit f6a9b8c
Show file tree
Hide file tree
Showing 20 changed files with 147 additions and 4 deletions.
2 changes: 1 addition & 1 deletion utils/archiveutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func ExtractArchive(localPath, localFileName, originFileName, logMsgPrefix strin
if err != nil {
return err
}
err = os.MkdirAll(extractionPath, 0777)
err = os.MkdirAll(extractionPath, 0755)
if errorutils.CheckError(err) != nil {
return err
}
Expand Down
85 changes: 82 additions & 3 deletions utils/io/fileutils/archive.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package fileutils

import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"path"
"strings"

"github.com/jfrog/jfrog-client-go/utils/errorutils"
"github.com/mholt/archiver/v3"
Expand All @@ -18,18 +23,24 @@ func IsSupportedArchive(filePath string) bool {
}

// The 'archiver' dependency includes an API called 'Unarchive' to extract archive files. This API uses the archive file
// extension to determine the archive type.// the local file path to extract the archive.
// extension to determine the archive type.
// We therefore need to use the file name as it was in Artifactory, and not the file name which was downloaded. To achieve this,
// we added a new implementation of the 'Unarchive' func and use it instead of the default one.
// localArchivePath - The local file path to extract the archive
// originArchiveName - The archive file name
// destinationPath - The extraction destination directory
func Unarchive(localArchivePath, originArchiveName, destinationPath string) error {
uaIface, err := byExtension(originArchiveName)
archive, err := byExtension(originArchiveName)
if err != nil {
return err
}
u, ok := uaIface.(archiver.Unarchiver)
u, ok := archive.(archiver.Unarchiver)
if !ok {
return errorutils.CheckError(errors.New("format specified by source filename is not an archive format: " + originArchiveName))
}
if err = inspectArchive(archive, localArchivePath, destinationPath); err != nil {
return err
}
return u.Unarchive(localArchivePath, destinationPath)
}

Expand Down Expand Up @@ -118,3 +129,71 @@ var extCheckers = []archiver.ExtensionChecker{
&archiver.Xz{},
&archiver.Zstd{},
}

// Make sure the archive is free from Zip Slip and Zip symlinks attacks
func inspectArchive(archive interface{}, localArchivePath, destinationDir string) error {
walker, ok := archive.(archiver.Walker)
if !ok {
return errorutils.CheckError(errors.New("couldn't inspect archive: " + localArchivePath))
}
return walker.Walk(localArchivePath, func(archiveEntry archiver.File) error {
header, err := extractArchiveEntryHeader(archiveEntry)
if err != nil {
return err
}
if !isEntryInDestination(destinationDir, header.EntryPath) {
return errorutils.CheckError(errors.New("illegal path in archive: " + header.EntryPath))
}

if (archiveEntry.Mode() & os.ModeSymlink) != 0 {
err = checkSymlinkEntry(header, archiveEntry, destinationDir)
}
return err
})
}

// Make sure the extraction path of the symlink entry target is under the destination dir
func checkSymlinkEntry(header *archiveHeader, archiveEntry archiver.File, destinationDir string) error {
targetLinkPath := header.TargetLink
if targetLinkPath == "" {
// The link destination path is not always in the archive header
// In that case, we will look at the link content to get the link destination path
content, err := ioutil.ReadAll(archiveEntry.ReadCloser)
if err != nil {
return errorutils.CheckError(err)
}
targetLinkPath = string(content)
}

if !isEntryInDestination(destinationDir, targetLinkPath) {
return errorutils.CheckError(errors.New("illegal link path in archive: " + targetLinkPath))
}
return nil
}

// Make sure the extraction path of the archive entry is under the destination dir
func isEntryInDestination(destinationDir, pathInArchive string) bool {
// Since the entry in archive should be always represented as Unix path, the "path" module is used and not "filepath"
pathInArchive = path.Clean(pathInArchive)
if !path.IsAbs(pathInArchive) {
// If path is relative, concatenate it to the destination dir
pathInArchive = path.Join(destinationDir, pathInArchive)
}
return strings.HasPrefix(pathInArchive, destinationDir)
}

// Extract the header of the archive entry
func extractArchiveEntryHeader(f archiver.File) (*archiveHeader, error) {
headerBytes, err := json.Marshal(f.Header)
if err != nil {
return nil, err
}
archiveHeader := &archiveHeader{}
err = json.Unmarshal(headerBytes, archiveHeader)
return archiveHeader, err
}

type archiveHeader struct {
EntryPath string `json:"Name,omitempty"`
TargetLink string `json:"Linkname,omitempty"`
}
64 changes: 64 additions & 0 deletions utils/io/fileutils/archive_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package fileutils

import (
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
)

func TestUnarchive(t *testing.T) {
tests := []string{"zip", "tar", "tar.gz"}
for _, extension := range tests {
t.Run(extension, func(t *testing.T) {
// Create temp directory
tmpDir, err := CreateTempDir()
assert.NoError(t, err)
defer RemoveTempDir(tmpDir)

// Run unarchive on archive created on Unix
err = runUnarchive(t, "unix."+extension, "archives", filepath.Join(tmpDir, "unix"))
assert.NoError(t, err)
assert.FileExists(t, filepath.Join(tmpDir, "unix", "link"))
assert.FileExists(t, filepath.Join(tmpDir, "unix", "dir", "file"))

// Run unarchive on archive created on Windows
err = runUnarchive(t, "win."+extension, "archives", filepath.Join(tmpDir, "win"))
assert.NoError(t, err)
assert.FileExists(t, filepath.Join(tmpDir, "win", "link.lnk"))
assert.FileExists(t, filepath.Join(tmpDir, "win", "dir", "file.txt"))
})
}
}

func TestUnarchiveZipSlip(t *testing.T) {
tests := []struct {
testType string
archives []string
errorSuffix string
}{
{"rel", []string{"zip", "tar", "tar.gz"}, "illegal path in archive: ../file"},
{"abs", []string{"tar", "tar.gz"}, "illegal path in archive: /tmp/bla/file"},
{"softlink-abs", []string{"zip", "tar", "tar.gz"}, "illegal link path in archive: /tmp/bla/file"},
{"softlink-rel", []string{"zip", "tar", "tar.gz"}, "illegal link path in archive: ../file"},
}
for _, test := range tests {
t.Run(test.testType, func(t *testing.T) {
// Create temp directory
tmpDir, err := CreateTempDir()
assert.NoError(t, err)
defer RemoveTempDir(tmpDir)

for _, archive := range test.archives {
// Unarchive and make sure an error returns
err = runUnarchive(t, test.testType+"."+archive, "zipslip", tmpDir)
assert.Error(t, err)
assert.Contains(t, err.Error(), test.errorSuffix)
}
})
}
}

func runUnarchive(t *testing.T, archiveFileName, sourceDir, targetDir string) error {
return Unarchive(filepath.Join("testdata", sourceDir, archiveFileName), archiveFileName, targetDir)
}
Binary file added utils/io/fileutils/testdata/archives/unix.tar
Binary file not shown.
Binary file added utils/io/fileutils/testdata/archives/unix.tar.gz
Binary file not shown.
Binary file added utils/io/fileutils/testdata/archives/unix.zip
Binary file not shown.
Binary file added utils/io/fileutils/testdata/archives/win.tar
Binary file not shown.
Binary file added utils/io/fileutils/testdata/archives/win.tar.gz
Binary file not shown.
Binary file added utils/io/fileutils/testdata/archives/win.zip
Binary file not shown.
Binary file added utils/io/fileutils/testdata/zipslip/abs.tar
Binary file not shown.
Binary file added utils/io/fileutils/testdata/zipslip/abs.tar.gz
Binary file not shown.
Binary file added utils/io/fileutils/testdata/zipslip/rel.tar
Binary file not shown.
Binary file added utils/io/fileutils/testdata/zipslip/rel.tar.gz
Binary file not shown.
Binary file added utils/io/fileutils/testdata/zipslip/rel.zip
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added utils/io/fileutils/testdata/zipslip/softlink-abs.zip
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added utils/io/fileutils/testdata/zipslip/softlink-rel.zip
Binary file not shown.

0 comments on commit f6a9b8c

Please sign in to comment.