Go module substitution and vanity-domain attacks
TL;DR: Go modules are referenced by import path (URL-like). The path resolves via
go.sumchecksum + proxy + version. Attacks: vanity-domain takeover (the domain hosting the redirect is now attacker-controlled), GOPROXY rerouting,go.sumchecksum DB bypass,replacedirective smuggling, and capability-implicit compile-time exec viacgo/embed/init. Go’s static-build culture means a compromised dep ships directly into your binary.
Attack surface
1. Vanity-domain takeover
- Import
example.com/foo→ Go fetcheshttps://example.com/foo?go-get=1, expectsmetaredirect to a real repo. - If
example.comlapses → attacker registers it → controls the redirect. - Several known incidents on small personal domains.
- Audit:
go list -m allto enumerate every module; for each, resolve the host; check domain ownership.
2. replace directive abuse
go.modcanreplaceany module path with a local path or different module.- In a malicious PR:
replace github.com/legit/dep => github.com/attacker/dep v1.0.0. go.sumupdates accordingly. Reviewer sees onlygo.modandgo.sumdiff; if they don’t read thereplaceline carefully → ships.
3. GOPROXY substitution
- Default
GOPROXY=https://proxy.golang.org,direct. If attacker can MitM your CI’s proxy access or run a malicious proxy: - They serve a different module than the public source.
- The Go checksum database (
sum.golang.org) usually catches this — but ifGOSUMDB=offor attacker is in the build env, gone. - Audit: confirm
GOSUMDBnot disabled in CI; pinGOPROXY.
4. init() and cgo compile-time exec
- Every imported package’s
init()runs at program start. A compromised dep can:- Read filesystem on startup.
- Open a network listener.
- Modify other globals via reflection.
cgodirectives in deps compile C code at build time → potential RCE on build host (less likely; needs#cgo CFLAGSinjection).go:embeddirectives can embed any file under the module dir into your binary.
5. Module version confusion
+incompatibleversions, pseudo-versions (v0.0.0-20240101000000-abcdef), branch-style versions.- Attacker can publish a higher version on a stale module, causing
go mod tidyto upgrade silently.
6. Subdomain/subpath squatting
github.com/user/repo/v2vsgithub.com/user/repo— separate module paths.- Attacker publishes
v2of a popularv1-only module. Users whogo get github.com/user/repo/v2(typo or upgrade attempt) get attacker’s code.
7. Compromised maintainer account
- GitHub account takeover → push malicious tag/release.
- Go encourages immutable versions, but a force-push to an existing tag (rare but possible if tag protection is off) replaces the module.
8. Capability creep via transitive deps
gin-gonic/ginis fine; one of its 20 transitive deps is sketchy. Eachinit()runs in your process.go list -m -deps allenumerates the closure.
Audit workflow
Static
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Enumerate full dep graph
go list -m -deps -json all | jq '.Path, .Version, .Replace?.Path'
# Find all replace directives
grep -E '^replace' go.mod
# Find all init() functions in vendor/ (or after go mod vendor)
grep -rn 'func init()' vendor/ | head -20
# Find cgo directives
grep -rn '^// #cgo\|^/\*\n#cgo' vendor/
# Find embed directives
grep -rn '//go:embed' vendor/
Suspicious indicators
replacedirective pointing to a non-organisation repo.- Module hosted on a personal domain with no DNS-level continuity.
- Module first published <90 days, depended on by your transitive tree.
- Unusual
init()doing network/filesystem ops.
Tools
govulncheck— official Go vuln database; runs at build time. CI-friendly.- OSV-Scanner — broader CVE feed, includes Go.
gosec— SAST for your own code, catches dangerous patterns; partial supply-chain via dep manifest scanning.- Snyk Open Source / Socket for Go.
go mod verify— verifies all deps matchgo.sum. Run in CI.
Hardening
Build pipeline
GOFLAGS="-mod=readonly"in CI — nogo mod tidyside-effects.GOSUMDB=sum.golang.orgalways (don’t disable).- Vendored deps (
go mod vendor) + commitvendor/→ reproducible builds + auditable diff in PRs. - Container build with no internet beyond proxy.
Repo policy
- Lockstep
go.mod+go.sumin PRs; review any change. - Tag protection on GitHub repos.
- Pin to a single GOPROXY in CI.
- Require
go mod verifyto pass.
Pre-build screen
- Compare new
go.sumagainst allowlist of known checksums for known deps. - Alert on every new top-level dep.
- Audit transitive churn: a single
go getmay pull 10 new deps; review each.