VYPR
High severityNVD Advisory· Published Aug 15, 2025· Updated Aug 15, 2025

HashiCorp go-getter Vulnerable to Arbitrary Read through Symlink Attack

CVE-2025-8959

Description

HashiCorp's go-getter library subdirectory download feature is vulnerable to symlink attacks leading to unauthorized read access beyond the designated directory boundaries. This vulnerability, identified as CVE-2025-8959, is fixed in go-getter 1.7.9.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

A symlink vulnerability in HashiCorp's go-getter library allows reading files outside the intended directory via crafted subdirectory downloads.

Root

Cause

The vulnerability in HashiCorp's go-getter library, identified as CVE-2025-8959, arises from insufficient symlink validation during the subdirectory download feature. When a user downloads a subdirectory from a remote source (e.g., via Git or other protocols), go-getter copies the directory structure using copyDir. The original code resolved symlinks in the source path but did not verify that the resolved path stays within the intended directory [1]. This allowed an attacker to craft a repository with symlinks that point outside the expected boundaries.

Exploitation

An attacker can exploit this by hosting a repository (or other source) that contains a symlink pointing to an arbitrary file outside the target download directory. When a victim uses go-getter to download a subdirectory from that repository, the library will follow the symlink and copy the linked file to the destination. No authentication is required beyond access to the source, and the attack can be executed remotely if the victim downloads a user-controlled URL [1][2].

Impact

Successful exploitation results in unauthorized read access to files outside the designated directory. An attacker could read sensitive files from the victim's filesystem, such as configuration files or credentials, if those files can be referenced through a symlink. The go-getter library is used by Terraform and Nomad for downloading modules and binaries, making this a potential supply-chain risk [1].

Mitigation

HashiCorp has fixed the vulnerability in go-getter version 1.7.9 [2]. The fix, seen in commit 87541b2501c00df5eaedea6acc61a2a4a4efa5b7, adds a check in copyDir that validates the resolved symlink path does not escape the original source directory [3]. Users should upgrade to go-getter v1.7.9 or later. The Go vulnerability database also tracks this as GO-2025-3892 [4].

AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/hashicorp/go-getterGo
< 1.7.91.7.9

Affected products

2

Patches

1
87541b2501c0

fix: go-getter subdir paths (#540)

https://github.com/hashicorp/go-getterDeniz Onur DuzgunAug 9, 2025via ghsa
3 files changed · +76 9
  • copy_dir.go+13 9 modified
    @@ -24,11 +24,19 @@ func copyDir(ctx context.Context, dst string, src string, ignoreDot bool, disabl
     	// We can safely evaluate the symlinks here, even if disabled, because they
     	// will be checked before actual use in walkFn and copyFile
     	var err error
    -	src, err = filepath.EvalSymlinks(src)
    +	resolved, err := filepath.EvalSymlinks(src)
     	if err != nil {
     		return err
     	}
     
    +	// Check if the resolved path tries to escape upward from the original
    +	if disableSymlinks {
    +		rel, err := filepath.Rel(filepath.Dir(src), resolved)
    +		if err != nil || filepath.IsAbs(rel) || containsDotDot(rel) {
    +			return ErrSymlinkCopy
    +		}
    +	}
    +
     	walkFn := func(path string, info os.FileInfo, err error) error {
     		if err != nil {
     			return err
    @@ -42,12 +50,9 @@ func copyDir(ctx context.Context, dst string, src string, ignoreDot bool, disabl
     			if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink {
     				return ErrSymlinkCopy
     			}
    -			// if info.Mode()&os.ModeSymlink == os.ModeSymlink {
    -			// 	return ErrSymlinkCopy
    -			// }
     		}
     
    -		if path == src {
    +		if path == resolved {
     			return nil
     		}
     
    @@ -62,16 +67,15 @@ func copyDir(ctx context.Context, dst string, src string, ignoreDot bool, disabl
     
     		// The "path" has the src prefixed to it. We need to join our
     		// destination with the path without the src on it.
    -		dstPath := filepath.Join(dst, path[len(src):])
    +		dstPath := filepath.Join(dst, path[len(resolved):])
     
     		// If we have a directory, make that subdirectory, then continue
     		// the walk.
     		if info.IsDir() {
    -			if path == filepath.Join(src, dst) {
    +			if path == filepath.Join(resolved, dst) {
     				// dst is in src; don't walk it.
     				return nil
     			}
    -
     			if err := os.MkdirAll(dstPath, mode(0755, umask)); err != nil {
     				return err
     			}
    @@ -84,5 +88,5 @@ func copyDir(ctx context.Context, dst string, src string, ignoreDot bool, disabl
     		return err
     	}
     
    -	return filepath.Walk(src, walkFn)
    +	return filepath.Walk(resolved, walkFn)
     }
    
  • get_git.go+3 0 modified
    @@ -302,6 +302,9 @@ func (g *GitGetter) update(ctx context.Context, dst, sshKeyFile string, u *url.U
     
     // fetchSubmodules downloads any configured submodules recursively.
     func (g *GitGetter) fetchSubmodules(ctx context.Context, dst, sshKeyFile string, depth int) error {
    +	if g.client != nil {
    +		g.client.DisableSymlinks = true
    +	}
     	args := []string{"submodule", "update", "--init", "--recursive"}
     	if depth > 0 {
     		args = append(args, "--depth", strconv.Itoa(depth))
    
  • get_git_test.go+60 0 modified
    @@ -802,6 +802,66 @@ func TestGitGetter_subdirectory_symlink(t *testing.T) {
     
     }
     
    +func TestGitGetter_subdirectory_malicious_symlink(t *testing.T) {
    +	if !testHasGit {
    +		t.Skip("git not found, skipping")
    +	}
    +
    +	if runtime.GOOS == "windows" {
    +		t.Skip("skipping on windows since the test requires sh")
    +		return
    +	}
    +
    +	g := new(GitGetter)
    +	dst := tempDir(t)
    +
    +	repo := testGitRepo(t, "empty-repo")
    +	repo.git("config", "commit.gpgsign", "false")
    +
    +	// Create a malicious symlink that tries to escape the repository
    +	symlinkPath := filepath.Join(repo.dir, "root")
    +	if err := os.Symlink("../../../../../../../../../../../", symlinkPath); err != nil {
    +		t.Fatal(err)
    +	}
    +
    +	repo.git("add", symlinkPath)
    +	repo.git("commit", "-m", "Adding malicious symlink")
    +
    +	u, err := url.Parse(fmt.Sprintf("git::%s//root/etc/passwd", repo.url.String()))
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +
    +	client := &Client{
    +		Src: u.String(),
    +		Dst: dst,
    +		Pwd: ".",
    +
    +		Mode: ClientModeDir,
    +
    +		Detectors: []Detector{
    +			new(GitDetector),
    +		},
    +		Getters: map[string]Getter{
    +			"git": g,
    +		},
    +	}
    +
    +	err = client.Get()
    +	if err == nil {
    +		t.Fatalf("expected client get to fail")
    +	}
    +
    +	if _, err := os.Stat(filepath.Join(dst, "etc", "passwd")); err == nil {
    +		t.Fatalf("expected /etc/passwd to not exist in destination")
    +	}
    +
    +	if !errors.Is(err, ErrSymlinkCopy) {
    +		t.Fatalf("unexpected error: %v", err)
    +	}
    +
    +}
    +
     func TestGitGetter_subdirectory(t *testing.T) {
     	if !testHasGit {
     		t.Skip("git not found, skipping")
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.