How Go Prevents Supply Chain Attacks
What the LiteLLM poisoning reveals about go.sum quietly protecting you
March 24: LiteLLM got poisoned
At 10:39 UTC on March 24, 2026, LiteLLM was compromised. LiteLLM is the most popular unified LLM API library in the Python ecosystem.
The attacker first took over Trivy’s (a security scanning tool) GitHub Action, repointing the v0.69.4 tag to malicious code. LiteLLM’s CI/CD pipeline didn’t pin Trivy’s version, so it automatically pulled the poisoned Action. The malicious code stole the PYPI_PUBLISH token from the GitHub Actions runtime environment.
With the token in hand, the attacker published two malicious versions: 1.82.7 hid a Base64-encoded backdoor proxy_server.py that executed on import; 1.82.8 deployed a .pth file that ran when the Python interpreter started -- no import needed.
46 minutes. 47,000 downloads. The malicious code scraped environment variables, SSH keys, AWS/GCP/Azure credentials, Kubernetes tokens, and database passwords.

Three hours later, PyPI quarantined both versions. The LiteLLM team rotated credentials and brought in Google Mandiant for forensics.
See the LiteLLM Security Advisory and Snyk’s analysis for full details.
This pattern has played out before: event-stream (npm, 2018), XZ Utils (2024). Attackers don't touch your code. They compromise the code you trust. That's a supply chain attack.
Go rarely has this problem. Not by luck — by design.
Layer 1: go.sum + sumdb — trust no one, trust math
How do you know the package you downloaded today is the same one you got yesterday?
Python’s answer: trust PyPI. If PyPI says this is 1.82.7, then it’s 1.82.7.
Go’s answer: prove it.
go.sum records a cryptographic hash of every dependency. Computed and stored on the first go get, verified on every subsequent build. Hash mismatch? Build fails.


But go.sum has a hole: Trust On First Use (TOFU). On the first download, there's no historical hash to compare against (go.sum is empty). A man-in-the-middle attack can succeed in this window.
Go’s fix: sumdb (sum.golang.org) -- a global, public module hash notary service. Each module version has exactly one hash in sumdb, immutable once written. go get checks sumdb automatically. Hash mismatch? Build fails.
Under the hood, sumdb uses a Transparent Log built on a Merkle Tree.
Merkle Tree: why “immutable” isn’t just marketing

Each leaf node R0-R3 is a SHA-256 hash of a module version (e.g., golang.org/x/[email protected]). Nodes pair up, hash upward, and produce a single root hash.
The root hash is the tree’s fingerprint. Change any leaf, and the root hash changes completely — SHA-256’s avalanche effect. This is a cryptographic guarantee, not application logic.
To prevent attackers from crafting leaf nodes that impersonate internal nodes (second preimage attacks), sumdb prefixes each node type differently:
// golang.org/x/mod/sumdb/tlog source
func RecordHash(data []byte) Hash // leaf: prefix 0x00 + data
func NodeHash(left, right Hash) Hash // internal: prefix 0x01 + left + rightClients perform two types of verification with this tree:
Inclusion Proof — “Is this module version actually in the log?”
To verify R1 is in the tree, sumdb returns just 3 hashes: R0’s hash and H(23)’s hash. Your local go command computes R1 + R0 → H(01), then H(01) + H(23) → Root, and compares against the known root hash. Match means R1 is genuinely in sumdb.
For a tree with 100 million records, proof requires only 27 hashes (log₂(1⁰⁸) ≈ 27). Less than 1KB.
Consistency Proof — “Have you tampered with history?”
The client caches the last-seen tree size and root hash. On the next query, it demands sumdb prove: the new tree contains all old records in the same order. Mathematically, this means proving the old tree is a prefix of the new one.
If sumdb deletes an old version or modifies an old hash, the consistency proof fails. Client refuses to proceed. Build aborts.
These two proofs together mean: even if someone controls proxy.golang.org (Go's module proxy), they cannot forge sumdb records. The proxy only caches and distributes. Verification authority stays with sumdb and your local go command.
Technical details in Russ Cox’s Transparent Log design. sumdb follows the proof protocol from RFC 6962 (Certificate Transparency).
Engineering elegance: Tile sharding
A practical problem: dynamically computing Merkle proofs for every query would crush the server.
Go’s solution is Tile sharding — slice the Merkle Tree into fixed-size “tiles,” precompute them, and serve as static files.
The tree is cut at height H (sumdb uses H=8). Each tile covers ²⁸ = 256 hashes. Addressing format: /tile/H/L/K, where L is the level and K is the index.
# Actual sumdb tile requests
GET https://sum.golang.org/tile/8/0/x001/234 ← Level 0, tile #1234
GET https://sum.golang.org/tile/8/0/x001/234.p/5 ← Partial tile (only 5 hashes)Each complete tile stores 256 SHA-256 hashes, fixed at 8KB. Once full, it never changes.
A cryptographic protocol becomes a CDN problem:
- Complete tiles never expire — once full, content is fixed. Set
Cache-Control: immutable, cache globally forever - Clients fetch on demand — verifying one module version typically requires 3–4 tiles (~24KB), rebuilding the proof path locally
- Zero server computation — no dynamic proof generation per query.
sum.golang.orgcan be as simple as a file server
Without tiles, sumdb would need to dynamically compute Merkle proof paths for tens of thousands of go get requests per second. With tiles, all computation moves to the client. Mathematically equivalent, orders of magnitude cheaper to operate.
Tile implementation source: golang.org/x/mod/sumdb/tlog
Layer 2: MVS — the most conservative version picker
Go’s version selection algorithm is MVS (Minimal Version Selection).
npm, pip, and cargo default to the latest version that satisfies constraints. Go picks the minimum version.
Take the LiteLLM incident.
The attacker published malicious versions 1.82.7 and 1.82.8. If your requirements.txt says litellm>=1.80.0, pip install grabs 1.82.8—the latest, malicious version.
Go won’t. If your go.mod says require litellm v1.80.0, MVS gives you v1.80.0. No more, no less. Unless you explicitly run go get litellm@latest, new releases won't be included in your build.
How fast a malicious version spreads depends directly on how many users auto-upgrade to the latest version. MVS kills that auto-upgrade default.
Layer 3 (proposed): Dependency Cooldown
The Go community is discussing Dependency Cooldown.
Newly published module versions would be forced to wait N days before go get can pull them.
GOCOOLDOWN=15d go mod tidyThis tells the Go toolchain: automatically exclude any version published less than 15 days ago.
Most supply chain attacks are discovered within hours to days of publication. LiteLLM’s malicious versions survived 3 hours. A 15-day cooldown gives the security community plenty of time to catch and report problems.
Anti-tampering is built in: the cooldown clock starts from sumdb’s “first observed” timestamp, not the package’s Git tag date. Attackers can’t backdate a tag to bypass the cooldown.
Think of it like food quarantine. Fresh meat isn’t necessarily bad, but if you hold it for a few days, let the inspectors take a look.
Side by side: Go vs Python

Python isn’t without defenses. pip install --require-hashes can enforce hash verification. pip-audit can scan for known vulnerabilities. But these are opt-in. Developers must actively enable them.
Go inverts this: security is the default. Turning off verification (GONOSUMCHECK) requires an explicit declaration.
What your Go project should do
A few things worth checking:
Production CI configuration
# Lock dependencies, prevent auto-fetching during builds
GOFLAGS=-mod=readonly go build ./...
# Scan for known CVEs
govulncheck ./...-mod=readonly ensures builds only use versions recorded in go.sum. Missing dependency or hash mismatch? Error, not silent fix.
Private module configuration
# Public dependencies go through sumdb, skip for private modules
GONOSUMDB=corp.internal/*
GONOSUMCHECK=corp.internal/*
GOPRIVATE=corp.internal/*Keep GONOSUMCHECK scope as small as possible. Every excluded module is one more unverified dependency.
Dependency update discipline
- Don’t run
go get @lateston production branches - Use Dependabot / Renovate for automated PRs, merge after human review
- Watch the Go Vulnerability Database
Closing
Trivy Action compromised → LiteLLM CI infected → PyPI token leaked → malicious versions published → 47,000 downloads. Each step exploited a weak link in the trust chain.
Go’s module system didn’t dodge this by luck. sumdb’s Merkle Tree eliminates the TOFU problem at the cryptographic level. MVS blocks “auto-upgrade to malicious version” at the version selection level. The cooldown proposal (if accepted) will compress the attack window in the time dimension.
How big is your GONOSUMCHECK scope? Does your CI have -mod=readonly? Worth a look.
References
- LiteLLM — Security Update: Suspected Supply Chain Incident
- Snyk — How a Poisoned Security Scanner Became the Key to Backdooring LiteLLM
- Go Documentation — Module Authentication: Checksum Database
- Russ Cox — Transparent Log Design
- Go Proposal — Dependency Cooldown (Issue #76485)