[secrity] Zip-Slip in Archive Extraction
Root Cause
Gopeed's archive extraction has three independent zip-slip vectors, all in the pkg/download/ package.
Vector 1: 7z Multi-Part — Full Arbitrary File Write
pkg/download/extract_7z.go:46 uses f.Name from bodgit/sevenzip directly in filepath.Join with zero path sanitization:
for _, f := range reader.File {
destPath := filepath.Join(destDir, f.Name) // no sanitization
if f.FileInfo().IsDir() {
os.MkdirAll(destPath, f.Mode()) // dir escape
continue
}
os.MkdirAll(filepath.Dir(destPath), 0755)
extractSevenZipFile(f, destPath) // file escape
}Neither bodgit/sevenzip nor Gopeed sanitizes f.Name. An entry named ../../evil.txt writes to an arbitrary path outside the extraction directory. This path is reached when a user downloads or extracts any .7z.001 multi-part archive.
Vector 2: Directory Creation Escape in All Formats
pkg/download/extract.go:284-296 handles directory entries BEFORE the path sanitization check:
func extractFile(ctx context.Context, fileInfo archives.FileInfo, destDir string) error {
if fileInfo.IsDir() {
destPath := filepath.Join(destDir, fileInfo.NameInArchive) // line 287: unsanitized
return os.MkdirAll(destPath, fileInfo.Mode()) // line 288: escapes destDir
}
// Sanitize the path to prevent path traversal attacks
cleanPath := filepath.Clean(fileInfo.NameInArchive) // line 292: too late for dirs
if strings.HasPrefix(cleanPath, "..") || filepath.IsAbs(cleanPath) {
return nil
}
// ... file extraction with sanitized pathThe mholt/archives library explicitly documents that NameInArchive is not sanitized (archives.go:32-33: "should not be trusted at face value"). A directory entry named ../../escaped/ in any zip, tar, tar.gz, or rar archive creates directories outside the extraction target. This affects all formats routed through extractFile via createExtractionHandler.
Vector 3: Windows Updater — Full Arbitrary File Write + Execution
cmd/updater/updater_windows.go has two separate zip-slip bugs:
installByInstaller (line 48):
path := filepath.Join(tempDir, file.Name) // no sanitizationAfter extraction, the code searches for .exe/.msi files and EXECUTES them (line 84: exec.Command(installerPath)). A crafted update package achieves arbitrary code execution.
installByPortable (line 104):
path := filepath.Join(destDir, file.Name) // no sanitizationWrites files directly to the application's install directory with no path validation.
Both use Go's archive/zip reader directly, bypassing the extractFile function entirely.
Reproduction
Requires: Go 1.21+, Python 3 with py7zr (pip3 install py7zr), curl.
Run exploit.sh — it clones Gopeed, builds the real REST API server from source, starts it, then downloads malicious archives via the Gopeed HTTP download pipeline with AutoExtract enabled.
Vector 1 (7z arbitrary file write): Serves a .7z.001 archive with entry ../../FILE_ESCAPED/pwned.txt via HTTP. Gopeed downloads it, detects multi-part 7z, routes to extractSevenZipMultiPart which writes the file outside the download directory with zero sanitization.
Vector 2 (directory escape): Serves a tar.gz with directory entry ../../ESCAPED/ via HTTP. Gopeed downloads it, routes to extractArchive → extractFile, which processes directory entries before the path sanitization check and creates directories outside the download directory.
Vector 3 (Windows updater): Code path analysis — updater_windows.go:48,104 have zero sanitization. Requires a compromised or MITM'd update server to exploit; the updater extracts and EXECUTES the payload.
Impact
An attacker crafts a malicious archive (7z, zip, tar.gz, rar) and distributes it via any download link. When a Gopeed user downloads it with AutoExtract enabled (or manually extracts), files are written to arbitrary paths on the filesystem.
- Vector 1 (7z multi-part): Arbitrary file write. Attacker can overwrite
~/.bashrc,~/.ssh/authorized_keys, cron jobs, or application configuration. Severity: CRITICAL. - Vector 2 (directory escape): Arbitrary directory creation. Lower severity alone but enables preparation for follow-up attacks (creating
.ssh/, cron directories). Severity: MEDIUM. - Vector 3 (Windows updater): Arbitrary file write + code execution. Requires MITM or compromised update channel. Severity: CRITICAL (when reachable).
The AutoExtract feature (pkg/protocol/http/model.go:17) triggers extraction automatically on download completion, making exploitation zero-click once the user clicks a download link.
Suggested Fix
For Vector 1 (extract_7z.go): Add path sanitization before file write, consistent with extractFile:
cleanName := filepath.Clean(f.Name)
if strings.HasPrefix(cleanName, "..") || filepath.IsAbs(cleanName) {
continue
}
destPath := filepath.Join(destDir, cleanName)For Vector 2 (extract.go:287): Move the sanitization check BEFORE the directory handling:
func extractFile(ctx context.Context, fileInfo archives.FileInfo, destDir string) error {
cleanPath := filepath.Clean(fileInfo.NameInArchive)
if strings.HasPrefix(cleanPath, "..") || filepath.IsAbs(cleanPath) {
return nil
}
if fileInfo.IsDir() {
return os.MkdirAll(filepath.Join(destDir, cleanPath), fileInfo.Mode())
}
// ... existing file extractionFor Vector 3 (updater_windows.go): Add the same sanitization before each filepath.Join(tempDir/destDir, file.Name).
Additionally, add filepath.Rel(destDir, destPath) validation to ensure the resolved path stays within destDir, which guards against symlink-based escapes.
Source: GopeedLab/gopeed