CVE-2024-45388 (Go)

Hoverfly is a lightweight service virtualization/ API simulation / API mocking tool for developers and testers. The `/api/v2/simulation` POST handler allows users to create new simulation views from the contents of a user-specified file. This feature can be abused by an attacker to read arbitrary files from the Hoverfly server. Note that, although the code prevents absolute paths from being specified, an attacker can escape out of the `hf.Cfg.ResponsesBodyFilesPath` base path by using `../` segments and reach any arbitrary files. This issue was found using the Uncontrolled data used in path expression CodeQL query for python. Users are advised to make sure the final path (`filepath.Join(hf.Cfg.ResponsesBodyFilesPath, filePath)`) is contained within the expected base path (`filepath.Join(hf.Cfg.ResponsesBodyFilesPath, "/")`). This issue is also tracked as GHSL-2023-274.

Weakness:
CWE-200: name: Exposure of Sensitive Information to an Unauthorized Actor
description: The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.
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/SpectoLabs/hoverfly

Vulnerable code:
file_path: core/hoverfly_funcs.go
snippet: func (hf *Hoverfly) readResponseBodyFile(filePath string) (string, error) {
	if filepath.IsAbs(filePath) {
		return "", fmt.Errorf("bodyFile contains absolute path (%s). only relative is supported", filePath)
	}

	fileContents, err := ioutil.ReadFile(filepath.Join(hf.Cfg.ResponsesBodyFilesPath, filePath))
	if err != nil {
		return "", err
	}

	return string(fileContents[:]), nil
}

Fixed code:
file_path: core/hoverfly_funcs.go
snippet: func (hf *Hoverfly) readResponseBodyFile(filePath string) (string, error) {
	if filepath.IsAbs(filePath) {
		return "", fmt.Errorf("bodyFile contains absolute path (%s). only relative is supported", filePath)
	}

	resolvedPath, err := util.ResolveAndValidatePath(hf.Cfg.ResponsesBodyFilesPath, filePath)
	if err != nil {
		return "", err
	}

	fileContents, err := os.ReadFile(resolvedPath)
	if err != nil {
		return "", err
	}

	return string(fileContents[:]), nil
}

file_path: core/util/util.go
snippet: func ResolveAndValidatePath(basePath, relativePath string) (string, error) {
	absBasePath, err := filepath.Abs(basePath)
	if err != nil {
		return "", fmt.Errorf("failed to get absolute base path: %v", err)
	}

	cleanRelativePath := filepath.Clean(relativePath)

	// Check if the relative path starts with ".."
	if strings.HasPrefix(cleanRelativePath, "..") {
		return "", fmt.Errorf("relative path is invalid as it attempts to backtrack")
	}

	resolvedPath := filepath.Join(absBasePath, cleanRelativePath)

	// Verify that the resolved path is indeed a subpath of the base path
	finalPath, err := filepath.Rel(absBasePath, resolvedPath)
	if err != nil {
		return "", fmt.Errorf("failed to get relative path: %v", err)
	}

	if strings.HasPrefix(finalPath, "..") {
		return "", fmt.Errorf("resolved path is outside the base path")
	}

	return resolvedPath, nil
}