Create Zip Archive with Traversal Guard

Walk a directory into a zip file while rejecting entries that escape the root, preserving relative paths with forward slashes.

GoData Integrity

Code

snippet
package main

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

func WriteZip(sourceDir, target string) error {
	out, err := os.Create(target)
	if err != nil {
		return err
	}
	defer out.Close()

	archive := zip.NewWriter(out)
	defer archive.Close()

	root := filepath.Clean(sourceDir)
	return filepath.WalkDir(root, func(path string, entry os.DirEntry, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		if entry.IsDir() {
			return nil
		}
		relative, err := filepath.Rel(root, path)
		if err != nil {
			return err
		}
		if relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) {
			return fmt.Errorf("refusing path outside root: %s", relative)
		}
		info, err := entry.Info()
		if err != nil {
			return err
		}
		header, err := zip.FileInfoHeader(info)
		if err != nil {
			return err
		}
		header.Name = filepath.ToSlash(relative)
		header.Method = zip.Deflate

		writer, err := archive.CreateHeader(header)
		if err != nil {
			return err
		}
		file, err := os.Open(path)
		if err != nil {
			return err
		}
		_, copyErr := io.Copy(writer, file)
		closeErr := file.Close()
		if copyErr != nil {
			return copyErr
		}
		return closeErr
	})
}

func main() {
	source, err := os.MkdirTemp("", "zip-source-")
	if err != nil {
		panic(err)
	}
	defer os.RemoveAll(source)
	if err := os.WriteFile(filepath.Join(source, "hello.txt"), []byte("hello"), 0o644); err != nil {
		panic(err)
	}

	target := filepath.Join(os.TempDir(), "devexamples-demo.zip")
	defer os.Remove(target)
	if err := WriteZip(source, target); err != nil {
		panic(err)
	}

	reader, err := zip.OpenReader(target)
	if err != nil {
		panic(err)
	}
	defer reader.Close()
	for _, file := range reader.File {
		fmt.Println("archived:", file.Name)
	}
}