VNX-GO-013 – Go Zip/Tar Slip via Archive Entry Name
Overview
This rule flags Go code where filepath.Join receives an archive entry name (header.Name) without path traversal validation. A malicious archive with entries containing ../ sequences can write files to arbitrary filesystem locations when extracted, including overwriting executables, configuration files, or cron jobs. This maps to CWE-22: Improper Limitation of a Pathname to a Restricted Directory.
Severity: High | CWE: CWE-22 – Path Traversal | OWASP ASVS: V12.5 – File Download
Go idiom note: Go’s
archive/zipandarchive/tarpackages do NOT sanitise entry names — this is documented behaviour, and the responsibility for path validation lies entirely with the calling code. There is no safe default: the secure pattern (validating withfilepath.Relor rejecting..components) must be implemented explicitly. This is NOT Go idiomatic by default because the standard library deliberately leaves the policy to the application.
Why This Matters
Zip Slip was disclosed in 2018 and affected thousands of projects across every major language ecosystem. In Go, the standard archive/zip and archive/tar packages do not sanitise entry names — the application must validate them. A crafted archive with an entry named ../../../etc/cron.d/backdoor will extract to that exact path if the extraction code blindly joins the entry name with a destination directory. Arbitrary file write typically leads directly to remote code execution.
OWASP ASVS v4.0 requirement V12.5.2 requires that only explicitly allowed file types may be uploaded and processed. Requirement V12.1.2 requires that archives are validated to prevent path traversal. CAPEC-139 (Relative Path Traversal) and MITRE ATT&CK T1083 (File and Directory Discovery) are the primary attack classification references.
go vet does not detect this pattern. staticcheck does not have a dedicated Zip Slip check. Neither gosec nor the Go compiler will warn about the missing validation step.
What Gets Flagged
The rule fires when filepath.Join appears on the same line as an archive entry name reference (header.Name or Header.Name).
// FLAGGED: no traversal check on zip entry name
for _, f := range zipReader.File {
path := filepath.Join(destDir, f.Name) // Zip Slip!
outFile, _ := os.Create(path)
}
// FLAGGED: tar entry name joined without validation
for {
header, _ := tarReader.Next()
path := filepath.Join(destDir, header.Name) // traversal possible
os.MkdirAll(path, 0755)
}
Remediation
- Validate the resolved path stays within the target directory using
filepath.Rel. This is the most robust approach and handles both../sequences and absolute paths embedded in entry names:
// SAFE: validate path stays within destination
import (
"fmt"
"path/filepath"
"strings"
)
func safePath(destDir, entryName string) (string, error) {
// Clean joins and resolves all . and .. components
destPath := filepath.Clean(filepath.Join(destDir, entryName))
// Rel checks that destPath is actually under destDir
rel, err := filepath.Rel(destDir, destPath)
if err != nil || strings.HasPrefix(rel, "..") {
return "", fmt.Errorf("path traversal detected: %s", entryName)
}
return destPath, nil
}
// Usage in zip extraction
for _, f := range zipReader.File {
outPath, err := safePath(destDir, f.Name)
if err != nil {
return err // reject the malicious archive entry
}
// ... proceed with extraction to outPath
}
- Reject entries containing
..path components as an additional early guard. This is a simpler check suitable as a first line of defence:
// SAFE: reject entries with path traversal sequences
import "strings"
for _, entry := range zipReader.File {
if strings.Contains(entry.Name, "..") {
return fmt.Errorf("illegal archive entry: %s", entry.Name)
}
path := filepath.Join(destDir, entry.Name)
// ... extract safely
}
- Use
github.com/cyphar/filepath-securejoinfor a battle-tested secure join implementation. This third-party library providessecurejoin.SecureJoinwhich confines all joins to a root directory at the OS level, even against symlink attacks:
// SAFE: SecureJoin prevents all path escape attempts
import securejoin "github.com/cyphar/filepath-securejoin"
for _, f := range zipReader.File {
outPath, err := securejoin.SecureJoin(destDir, f.Name)
if err != nil {
return err
}
// outPath is guaranteed to be under destDir
}
- Cap archive size and entry count before extracting to prevent zip bomb denial-of-service alongside the path traversal fix:
const maxSize = 100 << 20 // 100 MiB
const maxEntries = 1000
if len(zipReader.File) > maxEntries {
return fmt.Errorf("archive has too many entries: %d", len(zipReader.File))
}
References
- CWE-22: Improper Limitation of a Pathname to a Restricted Directory
- OWASP Application Security Verification Standard v4.0 – V12 File and Resources
- OWASP Path Traversal
- OWASP Go Security Cheat Sheet
- Zip Slip Vulnerability (Snyk research)
- Go archive/zip package documentation
- Go archive/tar package documentation
- filepath-securejoin library
- CAPEC-139: Relative Path Traversal
- MITRE ATT&CK T1083 – File and Directory Discovery