#1525·gopeed

[secrity] Zip-Slip in Archive Extraction

Author: AAtomicalCreated Sep 17, 2026Updated Sep 17, 2026

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:

go
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:

go
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 path

The 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):

go
path := filepath.Join(tempDir, file.Name)   // no sanitization

After 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):

go
path := filepath.Join(destDir, file.Name)   // no sanitization

Writes 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 extractArchiveextractFile, 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.

Image

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:

go
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:

go
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 extraction

For 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.