Zip Slip archive path traversal

Zip Slip archive path traversal

Description

Using archive entry names as file paths without validation can write outside the extraction directory through paths such as ../. Absolute paths and links also require handling appropriate to the API and operating system.

Potential impact

  • Web content, configuration files or executables may be overwritten.
  • Depending on how overwritten files are used and on process permissions, this may cause code execution or service disruption.

Remediation

  • Create a new private extraction directory and keep actual file access within it.
  • When only flat filenames are needed, reject absolute, parent and nested paths, and nonregular entries such as symlinks.
  • Use exclusive creation to prevent overwrites, and handle open, copy and close errors.
  • Limit file counts and actual decompressed size too. Path validation does not prevent decompression bombs.

Examples

The before-excerpt assumes dest and ZIP entry file exist. The after-example extracts only regular files without subdirectories and assumes other code running as the same account cannot manipulate the temporary directory. Extraction-size limits are omitted.

Before

go
_, _ = os.Create(filepath.Join(dest, file.Name))

After

go
package example

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

func extractFlat(r *zip.Reader) (root string, err error) {
    root, err = os.MkdirTemp("", "xeize-unzip-")
    if err != nil {
        return "", err
    }
    createdRoot := root
    defer func() {
        if err != nil {
            _ = os.RemoveAll(createdRoot)
        }
    }()

    for _, file := range r.File {
        if !file.Mode().IsRegular() {
            return "", fmt.Errorf("unsupported archive entry: %q", file.Name)
        }

        name := filepath.Clean(file.Name)
        if name != file.Name || filepath.IsAbs(name) || name == "." || name == ".." ||
            name != filepath.Base(name) {
            return "", fmt.Errorf("invalid archive path: %q", file.Name)
        }

        in, err := file.Open()
        if err != nil {
            return "", err
        }
        out, err := os.OpenFile(filepath.Join(root, name),
            os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
        if err != nil {
            in.Close()
            return "", err
        }

        _, copyErr := io.Copy(out, in)
        closeOutErr := out.Close()
        closeInErr := in.Close()
        if copyErr != nil {
            return "", copyErr
        }
        if closeOutErr != nil {
            return "", closeOutErr
        }
        if closeInErr != nil {
            return "", closeInErr
        }
    }
    return root, nil
}

Explanation:

  • Before: A file is created without checking that the joined path remains within the destination.
  • After: Names changed by normalization and names with more than one component are rejected; O_EXCL prevents duplicate creation. On error, cleanup is attempted using a separately retained directory path. The caller is responsible for cleanup after success.

References