Skip to content

fix: prevent path traversal during plugin extraction#755

Open
jcj46548-ux wants to merge 3 commits into
langgenius:mainfrom
jcj46548-ux:fix-zip-extract-path-traversal
Open

fix: prevent path traversal during plugin extraction#755
jcj46548-ux wants to merge 3 commits into
langgenius:mainfrom
jcj46548-ux:fix-zip-extract-path-traversal

Conversation

@jcj46548-ux

Copy link
Copy Markdown

Summary

  • validate zip entry paths before extracting plugin packages
  • reject parent-directory and backslash paths so entries cannot escape the target working directory
  • preserve normal nested file extraction, including packages with explicit directory entries

Tests

  • go test ./pkg/plugin_packager/decoder ./pkg/plugin_packager/packager

Notes:

  • go test ./pkg/plugin_packager is blocked locally because pkg/license/private_key/PRIVATE_KEY.pem is not present in the checkout.
  • go test -race ./pkg/plugin_packager/decoder is blocked locally because the current Go environment has cgo disabled.

@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. bug Something isn't working go Pull requests that update go code plugin-daemon labels Jun 6, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the ZIP extraction logic in ZipPluginDecoder to prevent path traversal vulnerabilities (Zip Slip) by introducing path validation helpers (safeExtractPath and pathIsInside) and adds corresponding unit tests. The review feedback suggests wrapping the file reader in an io.LimitReader to protect against Zip Bomb attacks, and explicitly checking for .. in the path validation to ensure robust path traversal prevention.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread pkg/plugin_packager/decoder/zip.go Outdated
Comment on lines +337 to +341
if _, err := io.Copy(writer, reader); err != nil {
reader.Close()
writer.Close()
return err
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

To protect against Zip Bomb (Denial of Service) attacks where a malicious ZIP file contains manipulated headers (e.g., declaring a small UncompressedSize64 but actually decompressing to gigabytes of data), you should wrap the file reader in an io.LimitReader using the declared UncompressedSize64. This ensures that the extraction process will not decompress more data than the header claims, preventing disk exhaustion.

			limitedReader := io.LimitReader(reader, int64(file.UncompressedSize64))
			if _, err := io.Copy(writer, limitedReader); err != nil {
				reader.Close()
				writer.Close()
				return err
			}

Comment thread pkg/plugin_packager/decoder/zip.go Outdated
Comment on lines +363 to +368
if entryPath == "." ||
path.IsAbs(entryPath) ||
strings.HasPrefix(entryPath, "../") ||
strings.Contains(entryPath, "/../") ||
strings.Contains(entryPath, `\`) ||
filepath.IsAbs(filepath.FromSlash(entryPath)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current path traversal validation checks if entryPath starts with ../ using strings.HasPrefix(entryPath, "../"). However, if entryPath is exactly .. (which can happen if the entry name is .. or resolves to it via path.Clean), it will bypass this check because it does not start with ../. While pathIsInside will eventually catch and reject it, it is safer, more explicit, and more efficient to reject .. directly in the first validation block.

Suggested change
if entryPath == "." ||
path.IsAbs(entryPath) ||
strings.HasPrefix(entryPath, "../") ||
strings.Contains(entryPath, "/../") ||
strings.Contains(entryPath, `\`) ||
filepath.IsAbs(filepath.FromSlash(entryPath)) {
if entryPath == "." ||
entryPath == ".." ||
path.IsAbs(entryPath) ||
strings.HasPrefix(entryPath, "../") ||
strings.Contains(entryPath, "/../") ||
strings.Contains(entryPath, `\\`) ||
filepath.IsAbs(filepath.FromSlash(entryPath)) {

Comment thread pkg/plugin_packager/decoder/zip.go Outdated
return nil
}

func safeExtractPath(dst, entryName string) (string, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

 func safeExtractPath(dst, entryName string) (string, error) {
  	if entryName == "" || strings.Contains(entryName, `\`) {
  		return "", fmt.Errorf("%w: %q", errUnsafeZipPath, entryName)
  	}

  	for _, part := range strings.Split(entryName, "/") {
  		if part == ".." {
  			return "", fmt.Errorf("%w: %q", errUnsafeZipPath, entryName)
  		}
  	}

  	entryPath := path.Clean(entryName)
  	if entryPath == "." || path.IsAbs(entryPath) {
  		return "", fmt.Errorf("%w: %q", errUnsafeZipPath, entryName)
  	}

  	targetPath := filepath.Join(dst, filepath.FromSlash(entryPath))
  	if !pathIsInside(dst, targetPath) {
  		return "", fmt.Errorf("%w: %q", errUnsafeZipPath, entryName)
  	}

  	return targetPath, nil
  }

filename = filepath.Join(workingPath, filename)
if z.reader == nil {
return z.err
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

root, err := os.OpenRoot(dst)
  	if err != nil {
  		return err
  	}
  	defer root.Close()

  	for _, file := range z.reader.File {
  		entryPath, err := safeEntryPath(file.Name)
  		if err != nil {
  			return err
  		}

  		if file.FileInfo().IsDir() {
  			if err := root.MkdirAll(entryPath, 0755); err != nil {
  				return err
  			}
  			continue
  		}

  		if err := root.MkdirAll(path.Dir(entryPath), 0755); err != nil {
  			return err
  		}

  		if err := extractZipFile(root, entryPath, file); err != nil {
  			return err
  		}
  	}

  	return nil

@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. and removed size:L This PR changes 100-499 lines, ignoring generated files. labels Jun 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working go Pull requests that update go code plugin-daemon size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants