CVE-2021-29417 (Go)

gitjacker before 0.1.0 allows remote attackers to execute arbitrary code via a crafted .git directory because of directory traversal.

Weakness:
CWE-22: name: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.

Repository: https://github.com/liamg/gitjacker

Vulnerable code:
file_path: internal/pkg/gitjacker/retriever.go
snippet: func (r *retriever) downloadFile(path string) error {

	path = strings.TrimSpace(path)

	filePath := filepath.Join(r.outputDir, ".git", path)

	if r.downloaded[path] {
		return nil
	}
	r.downloaded[path] = true

	relative, err := url.Parse(path)
	if err != nil {
		return err
	}

	absolute := r.baseURL.ResolveReference(relative)
	resp, err := r.http.Get(absolute.String())
	if err != nil {
		return fmt.Errorf("failed to retrieve %s: %w", absolute.String(), err)
	}
	defer func() { _ = resp.Body.Close() }()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("unexpected status code for url %s : %d", absolute.String(), resp.StatusCode)
	}

	content, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return err
	}

	if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
		return err
	}

	if !strings.HasSuffix(path, "/") {
		if err := ioutil.WriteFile(filePath, content, 0640); err != nil {
			return fmt.Errorf("failed to write %s: %w", filePath, err)
		}
	}

	switch path {
	case "HEAD":
		ref := strings.TrimPrefix(string(content), "ref: ")
		if err := r.downloadFile(ref); err != nil {
			return err
		}
		return nil
	case "config":
		return r.analyseConfig(content)
	case "objects/pack/":
		// parse the directory listing
		packFiles := packLinkRegex.FindAllStringSubmatch(string(content), -1)
		for _, packFile := range packFiles {
			if len(packFile) <= 1 {
				continue
			}
			if err := r.downloadFile(fmt.Sprintf("objects/pack/%s", packFile[1])); err != nil {
				logrus.Debugf("Failed to retrieve pack file %s: %s", packFile[1], err)
				continue
			}
		}
		return nil
	case "objects/info/packs":
		return r.parsePackMetadata(content)
	}

	if strings.HasSuffix(path, ".pack") {
		return r.parsePackFile(path, content)
	}

	if strings.HasPrefix(path, "refs/heads/") {
		if _, err := r.downloadObject(string(content)); err != nil {
			return err
		}
		return nil
	}

	hash := filepath.Base(filepath.Dir(path)) + filepath.Base(path)

	objectType, err := r.getObjectType(hash)
	if err != nil {
		return err
	}

	switch objectType {
	case GitCommitFile:

		commit, err := r.readCommit(hash)
		if err != nil {
			return err
		}

		logrus.Debugf("Successfully retrieved commit %s.", hash)

		if commit.Tree != "" {
			if _, err := r.downloadObject(commit.Tree); err != nil {
				logrus.Debugf("Object %s is missing and likely packed.", commit.Tree)
			}
		}
		for _, parent := range commit.Parents {
			if _, err := r.downloadObject(parent); err != nil {
				logrus.Debugf("Object %s is missing and likely packed.", parent)
			}
		}

	case GitTreeFile:

		tree, err := r.readTree(hash)
		if err != nil {
			return err
		}

		logrus.Debugf("Successfully retrieved tree %s.", hash)

		for _, subHash := range tree.Objects {
			if _, err := r.downloadObject(subHash); err != nil {
				logrus.Debugf("Object %s is missing and likely packed.", subHash)
			}
		}
	case GitBlobFile:
		logrus.Debugf("Successfully retrieved blob %s.", hash)
	default:
		return fmt.Errorf("unknown git file type for %s: %s", path, objectType)
	}

	return nil
}

Fixed code:
file_path: internal/pkg/gitjacker/retriever.go
snippet: func (r *retriever) downloadFile(path string) error {

	path = strings.TrimSpace(path)

	filePath := filepath.Join(r.outputDir, ".git", filepath.FromSlash(filepath.Clean("/"+path)))

	if r.downloaded[path] {
		return nil
	}
	r.downloaded[path] = true

	relative, err := url.Parse(path)
	if err != nil {
		return err
	}

	absolute := r.baseURL.ResolveReference(relative)
	resp, err := r.http.Get(absolute.String())
	if err != nil {
		return fmt.Errorf("failed to retrieve %s: %w", absolute.String(), err)
	}
	defer func() { _ = resp.Body.Close() }()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("unexpected status code for url %s : %d", absolute.String(), resp.StatusCode)
	}

	content, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return err
	}

	if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
		return err
	}

	if !strings.HasSuffix(path, "/") {
		if err := ioutil.WriteFile(filePath, content, 0640); err != nil {
			return fmt.Errorf("failed to write %s: %w", filePath, err)
		}
	}

	switch path {
	case "HEAD":
		ref := strings.TrimPrefix(string(content), "ref: ")
		if err := r.downloadFile(ref); err != nil {
			return err
		}
		return nil
	case "config":
		return r.analyseConfig(content)
	case "objects/pack/":
		// parse the directory listing
		packFiles := packLinkRegex.FindAllStringSubmatch(string(content), -1)
		for _, packFile := range packFiles {
			if len(packFile) <= 1 {
				continue
			}
			if err := r.downloadFile(fmt.Sprintf("objects/pack/%s", packFile[1])); err != nil {
				logrus.Debugf("Failed to retrieve pack file %s: %s", packFile[1], err)
				continue
			}
		}
		return nil
	case "objects/info/packs":
		return r.parsePackMetadata(content)
	}

	if strings.HasSuffix(path, ".pack") {
		return r.parsePackFile(path, content)
	}

	if strings.HasPrefix(path, "refs/heads/") {
		if _, err := r.downloadObject(string(content)); err != nil {
			return err
		}
		return nil
	}

	hash := filepath.Base(filepath.Dir(path)) + filepath.Base(path)

	objectType, err := r.getObjectType(hash)
	if err != nil {
		return err
	}

	switch objectType {
	case GitCommitFile:

		commit, err := r.readCommit(hash)
		if err != nil {
			return err
		}

		logrus.Debugf("Successfully retrieved commit %s.", hash)

		if commit.Tree != "" {
			if _, err := r.downloadObject(commit.Tree); err != nil {
				logrus.Debugf("Object %s is missing and likely packed.", commit.Tree)
			}
		}
		for _, parent := range commit.Parents {
			if _, err := r.downloadObject(parent); err != nil {
				logrus.Debugf("Object %s is missing and likely packed.", parent)
			}
		}

	case GitTreeFile:

		tree, err := r.readTree(hash)
		if err != nil {
			return err
		}

		logrus.Debugf("Successfully retrieved tree %s.", hash)

		for _, subHash := range tree.Objects {
			if _, err := r.downloadObject(subHash); err != nil {
				logrus.Debugf("Object %s is missing and likely packed.", subHash)
			}
		}
	case GitBlobFile:
		logrus.Debugf("Successfully retrieved blob %s.", hash)
	default:
		return fmt.Errorf("unknown git file type for %s: %s", path, objectType)
	}

	return nil
}