在Go中解压压缩文件的方法

1,772 阅读4分钟

要使用标准库在Go中解压一个压缩档案,你需要使用 archive/zip包并使用 zip.OpenReader(name string)函数打开文件。以这种方式提取ZIP文件,需要遍历所有的存档文件。对于每一个文件,我们在目标路径中创建一个新的空文件或目录,然后在那里解压其字节内容。

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

它是如何工作的

  1. 打开ZIP文件
// 1. Open the zip file
reader, err := zip.OpenReader(source)
if err != nil {
    return err
}
defer reader.Close()

要解压该文件,首先要用 zip.OpenReader(name string)函数打开它。像往常一样,在处理文件时,如果你不再需要它,记得要关闭它,在这种情况下使用 ReadCloser.Close()方法。

  1. 获取绝对目标路径
// 2. Get the absolute destination path
destination, err = filepath.Abs(destination)
if err != nil {
    return err
}

将我们的相对destination 路径转换为绝对表示,这在Zip Slip漏洞检查步骤中是需要的。

  1. 遍历存档内的压缩文件,并逐一解压
// 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
    }
}

在Go中使用的解压缩文件的实际过程是 archive/zip的实际解压过程是对打开的ZIP文件进行迭代,并将每个文件单独解压到最终目的地。

  1. 检查文件路径是否不受Zip Slip的影响
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)
    }

单个文件解压功能的第一步是检查这个文件的路径是否没有利用Zip Slip漏洞,这个漏洞在2018年被发现,影响了成千上万的项目。通过持有目录穿越文件名的特制档案,例如../../evil.sh ,攻击者可以获得对解压文件应该所在的目标文件夹之外的文件系统部分的访问。然后,攻击者可以覆盖可执行文件和其他敏感资源,对受害者的机器造成重大损害。

为了检测这个漏洞,通过结合destination 和ZIP压缩包内的文件名来准备目标文件路径。它可以使用 filepath.Join()函数来完成。然后我们检查这个最终文件路径是否包含我们的destination 路径作为前缀。如果没有,该文件可能是试图访问文件系统的一部分,而不是destination ,应该被拒绝。

例如,当我们想把我们的文件解压到/a/b/ 目录。

```go
err := unzipSource("testFolder.zip", "/a/b")
if err != nil {
log.Fatal(err)
}
```

而在存档中,有一个名字为../../../../evil.sh 的文件,那么

filepath.Join("/a/b", "../../../../evil.sh")

/evil.sh

这样,攻击者就可以在根目录/ 中解压evil.sh 文件,这在我们的检查中应该是不允许的。

  1. 创建一个目录树
// 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
}

对于ZIP档案中的每个文件或目录,我们需要在destination 路径中创建一个相应的目录,以便提取的文件的结果目录树与ZIP中的目录树相匹配。我们使用 os.MkdirAll()函数来完成这个任务。对于目录,我们在destination 路径中创建相应的文件夹,而对于文件,我们创建文件的基本目录。注意,当文件是一个目录时,我们从函数中返回,因为只有文件需要解压,我们将在接下来的步骤中进行。

  1. 为解压后的内容创建一个目标文件
// 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()

在解压缩ZIP档案文件之前,我们需要创建一个目标文件,以保存提取的内容。由于这个目标文件的模式应该与存档文件的模式一致,我们使用 os.OpenFile()函数,在这里我们可以将模式作为一个参数来设置。

  1. 解压缩一个文件的内容并将其复制到目标文件中
// 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

在最后一步,我们打开一个单独的ZIP文件,将其内容复制到上一步创建的文件中。用 zip.File.Open()可以在复制时访问档案文件的未压缩数据。