HashiCorp go-getter Vulnerable to Arbitrary Read through Symlink Attack
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.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/hashicorp/go-getterGo | < 1.7.9 | 1.7.9 |
Affected products
2- HashiCorp/Shared libraryv5Range: 0
Patches
187541b2501c0fix: go-getter subdir paths (#540)
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- github.com/advisories/GHSA-wjrx-6529-hcj3ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-8959ghsaADVISORY
- discuss.hashicorp.com/t/hcsec-2025-23-hashicorp-go-getter-vulnerable-to-arbitrary-read-through-symlink-attack/76242ghsaWEB
- github.com/hashicorp/go-getter/commit/87541b2501c00df5eaedea6acc61a2a4a4efa5b7ghsaWEB
- pkg.go.dev/vuln/GO-2025-3892ghsaWEB
News mentions
0No linked articles in our index yet.