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_EXCLprevents duplicate creation. On error, cleanup is attempted using a separately retained directory path. The caller is responsible for cleanup after success.