To unzip a compressed archive in Go using the standard library, you need to use archive/zip
package and open the file with the zip.OpenReader(name string)
function. Extracting a ZIP file in this way involves iterating through all the archive files. For each of them, we create a new empty file or directory in the target path and then uncompress its byte content there.
You can also check our example on how to zip a file in Go
|
|
How it works
- Open the zip file
14 15 16 17 18 19
// 1. Open the zip file reader, err := zip.OpenReader(source) if err != nil { return err } defer reader.Close()
To unzip the file, first, open it with the
zip.OpenReader(name string)
function. As always, when working with files, remember to close it if you no longer need it, using theReadCloser.Close()
method in this case. - Get the absolute destination path
21 22 23 24 25
// 2. Get the absolute destination path destination, err = filepath.Abs(destination) if err != nil { return err }
Convert our relative
destination
path to the absolute representation, which will be needed in the step of Zip Slip vulnerability checking. - Iterate over zip files inside the archive and unzip each of them
27 28 29 30 31 32 33
// 3. Iterate over zip files inside the archive and unzip each of them for _, f := range reader.File { err := unzipFile(f, destination) if err != nil { return err } }
The actual process of unzipping files in Go using
archive/zip
is to iterate through the files of the opened ZIP file and unpack each one individually to its final destination. - Check if file paths are not vulnerable to Zip Slip
38 39 40 41 42 43
func unzipFile(f *zip.File, destination string) error { // 4. Check if file paths are not vulnerable to Zip Slip filePath := filepath.Join(destination, f.Name) if !strings.HasPrefix(filePath, filepath.Clean(destination)+string(os.PathSeparator)) { return fmt.Errorf("invalid file path: %s", filePath) }
The first step of an individual file unzipping function is to check whether the path of this file does not make use of the Zip Slip vulnerability, which was discovered in 2018 and affected thousands of projects. With a specially crafted archive that holds directory traversal filenames, e.g.,
../../evil.sh
, an attacker can gain access to parts of the file system outside of the target folder in which the unzipped files should reside. The attacker can then overwrite executable files and other sensitive resources, causing significant damage to the victim machine.To detect this vulnerability, prepare the target file path by combining the
destination
and the name of the file inside the ZIP archive. It can be done usingfilepath.Join()
function. Then we check if this final file path contains ourdestination
path as a prefix. If not, the file may be trying to access the part of the file system other than thedestination
and should be rejected.For example, when we want to unzip our file into the
/a/b/
directory:err := unzipSource("testFolder.zip", "/a/b") if err != nil { log.Fatal(err) }
and in the archive there is a file with a name
../../../../evil.sh
, then the output offilepath.Join("/a/b", "../../../../evil.sh")
is
/evil.sh
In this way, the attacker can unzip the
evil.sh
file in the root directory/
, which should not be allowed with our check. - Create a directory tree
45 46 47 48 49 50 51 52 53 54 55
// 5. Create a directory tree if f.FileInfo().IsDir() { if err := os.MkdirAll(filePath, os.ModePerm); err != nil { return err } return nil } if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil { return err }
For each file or directory in the ZIP archive, we need to create a corresponding directory in the
destination
path, so that the resulting directory tree of the extracted files matches the directory tree inside the ZIP. We useos.MkdirAll()
function to do this. For directories, we create the corresponding folder in thedestination
path, and for files, we create the base directory of the file. Note that we return from the function when the file is a directory as only files need to be unzipped, which we will do in the next steps. - Create a destination file for unzipped content
57 58 59 60 61 62
// 6. Create a destination file for unzipped content destinationFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) if err != nil { return err } defer destinationFile.Close()
Before uncompressing a ZIP archive file, we need to create a target file where the extracted content could be saved. Since the mode of this target file should match the mode of the file inside the archive, we use
os.OpenFile()
function, where we can set the mode as an argument. - Unzip the content of a file and copy it to the destination file
64 65 66 67 68 69 70 71 72 73 74
// 7. Unzip the content of a file and copy it to the destination file zippedFile, err := f.Open() if err != nil { return err } defer zippedFile.Close() if _, err := io.Copy(destinationFile, zippedFile); err != nil { return err } return nil
In the last step, we open an individual ZIP file and copy its content to the file created in the previous step. Opening with
zip.File.Open()
gives access to the uncompressed data of the archive file while copying.