🗄️ Unzip a file in Go

compression file zip

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
package main

import (
    "archive/zip"
    "fmt"
    "io"
    "log"
    "os"
    "path/filepath"
    "strings"
)

func unzipSource(source, destination string) error {
    // 1. Open the zip file
    reader, err := zip.OpenReader(source)
    if err != nil {
        return err
    }
    defer reader.Close()

    // 2. Get the absolute destination path
    destination, err = filepath.Abs(destination)
    if err != nil {
        return err
    }

    // 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
        }
    }

    return nil
}

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)
    }

    // 5. Create 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
    }

    // 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()

    // 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
}

func main() {
    err := unzipSource("testFolder.zip", "")
    if err != nil {
        log.Fatal(err)
    }
}

How it works

  1. 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 the ReadCloser.Close() method in this case.

  2. 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.

  3. 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.

  4. 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 using filepath.Join() function. Then we check if this final file path contains our destination path as a prefix. If not, the file may be trying to access the part of the file system other than the destination 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 of

    filepath.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.

  5. 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 use os.MkdirAll() function to do this. For directories, we create the corresponding folder in the destination 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.

  6. 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.

  7. 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.

🗜️ Zip a file in Go

Learn how to compress a file or folder into zip format
compression file zip

📁 Create a directory in Go

Learn how to create a single or a hierarchy of directories
introduction file

📎 Convert JSON to CSV in Go

Learn how to transform JSON file to CSV
introduction file json csv