Safeguard
Software Supply Chain Security

Typosquatting and malicious Go modules published to pkg.g...

How malicious Go modules exploit pkg.go.dev's open publishing model, real typosquatting attacks like steelpoor/tlsproxy, and why Go's module proxy makes cleanup so hard.

Vikram Iyer
Security Researcher
8 min read

In March 2024, a researcher poking around pkg.go.dev found a module that looked like a routine networking helper — except it quietly opened a reverse shell to a server registered overseas. It's one more entry in a growing list of malicious Go modules that have slipped past Go's almost entirely automated publishing pipeline and landed on the official package index without a single human reviewing the code. Unlike npm or PyPI, Go has no registry team vetting submissions: any developer with a public Git repository and a version tag can have their code indexed on pkg.go.dev within minutes. That openness is exactly why go typosquatting has become a reliable technique for attackers — clone a popular import path with one character changed, plant credential-stealing or backdoor code, and wait for a rushed go get. This piece breaks down how these attacks work, what's actually been found in the wild, and why pkg.go.dev security needs a different playbook than other package ecosystems.

What Exactly Counts as a Malicious Go Module?

A malicious Go module is a published Go package that contains code designed to harm, spy on, or gain unauthorized access to the systems that import it — not a legitimate library with an accidental bug, but code that behaves the way attackers intended it to. The behaviors researchers keep finding are consistent across incidents: reverse shells, credential and SSH key exfiltration, cryptominers dropped onto build servers, and obfuscated payloads that only activate under specific conditions (a particular OS, a particular environment variable, a particular date).

Go has one quirk that makes it an unusually attractive target for this: the init() function. In Go, any init() function inside an imported package runs automatically the moment the package is imported — before main() even starts, and whether or not the developer ever calls anything from that package. A malicious dependency doesn't need to be used to do damage; it just needs to be present in the import graph. That single language feature has shown up repeatedly as the delivery mechanism in real malicious Go module cases, because it turns a passive go get into guaranteed code execution.

How Does Go Typosquatting Actually Work on pkg.go.dev?

Go typosquatting works by exploiting the fact that Go import paths are just repository URLs, not curated names assigned by a registry. On npm or PyPI, a package name is a short string a maintainer registers centrally. On pkg.go.dev, the "name" is literally wherever the source lives — github.com/org/project — so attackers have several angles that don't exist elsewhere:

  • Character-level typos: publishing github.com/gorlla/mux or github.com/sirupsen1/logrus-style near-matches of heavily imported libraries, banking on a mistyped go get or a copy-paste error from a tutorial.
  • Organization swaps: standing up a personal account that mimics a well-known org name closely enough to pass a quick visual check in a go.mod diff.
  • Repo-jacking: claiming a GitHub username or repository that a legitimate project previously used and then abandoned or renamed, so old import paths in tutorials, forks, and pinned dependencies silently start resolving to attacker-controlled code.
  • Version-tag abuse: pushing a malicious commit and tagging it as a routine patch release (v1.4.3) on a repo that otherwise has a clean, boring commit history, so a cursory glance at the project doesn't raise flags.

Because pkg.go.dev indexes automatically the first time a module is fetched through the Go module proxy, none of these require approval from anyone. The module simply appears.

What Real Malicious Go Modules Have Been Discovered in the Wild?

Security teams have publicly caught and dissected several of these campaigns, and the details are instructive. In August 2023, researchers at Socket disclosed a malicious Go module, github.com/steelpoor/tlsproxy, that functioned as a backdoor targeting Linux x86-64 systems — once imported and run, it gave an attacker remote, near-root-level control of the infected host. What made the case a useful case study wasn't just the payload; it was what happened after discovery. Even after the malicious GitHub repository behind the module was taken down, the module itself remained fetchable through the Go module proxy, because proxy.golang.org caches module content the first time it's requested and serves that cached copy indefinitely for reproducible builds. Deleting the source didn't delete the distribution channel.

That single incident captures the recurring pattern researchers at Socket, Snyk, and Phylum have flagged across dozens of smaller reports since 2021: a module with a plausible-sounding, typosquatted or newly-registered import path, a short-lived GitHub account behind it, and an init()-triggered payload built to exfiltrate data or open network access. The Go vulnerability database at vuln.go.dev, launched in April 2022, now tracks a portion of these once they're publicly disclosed — but that database is reactive by design, built on after-the-fact reports, not pre-publication screening.

Why Is pkg.go.dev Security Different From npm or PyPI?

pkg.go.dev security relies on cryptographic integrity checking rather than centralized content review, and that distinction matters more than it sounds. Since Go 1.13, released in 2019, the toolchain has shipped with a module proxy (GOPROXY) and a checksum database (GOSUMDB, backed by sum.golang.org) that verify a module's content hasn't changed since the first time anyone fetched it, recorded in your project's go.sum. That's a real security improvement — it stops a package from being silently swapped out or altered after the fact (a classic supply chain tampering vector).

What it does not do is evaluate whether the code was malicious at the moment of first publication. If a typosquatted or backdoored module is the first version ever fetched, its checksum gets recorded as "trusted" and every subsequent build that pulls it will verify cleanly, because the checksum only proves the code matches what was originally seen — not that what was originally seen was safe. npm and PyPI have their own well-documented typosquatting problems too, but both platforms at least run some automated malware scanning and support faster takedown of a package name once it's reported. Go's model, by design, has no equivalent central point where a module can be un-published from every consumer at once.

Why Are Golang Supply Chain Attacks So Hard to Undo Once Published?

Golang supply chain attacks are hard to undo because the module proxy that makes Go builds reproducible is the same mechanism that makes malicious modules durable. Reproducibility was the whole point of introducing the proxy and sum database in 2019 — a build run today should fetch the exact same code as a build run five years from now, even if the original repository is deleted, renamed, or rewritten. That's a genuinely good engineering property for legitimate dependencies.

Applied to a malicious module, it becomes a liability: once a version has been fetched once and cached by proxy.golang.org, it can keep being served to new consumers indefinitely, independent of what happens to the source repository. A defender who spots a malicious package and gets the GitHub repo suspended has not actually removed the threat from circulation — anyone with the module path and a compatible go.sum entry can still pull the cached, malicious copy. That gap between "the source is gone" and "the artifact is gone" is the core reason these incidents tend to have a longer tail than similar attacks on registries with a central takedown lever.

How Safeguard Helps

This is precisely the gap Safeguard is built to close for teams shipping Go. Rather than trusting that a module is safe because it resolved cleanly through go.sum, Safeguard inspects the actual behavior and provenance of every dependency entering your build: flagging import paths that are one edit-distance away from well-known modules, surfacing newly created or recently transferred repositories behind a go.mod entry, and calling out packages that ship init()-time network calls, shell execution, or credential access patterns before they ever reach a production build.

Safeguard also treats the proxy-caching problem as a first-class risk rather than an edge case — continuously monitoring your dependency tree against threat intelligence on known malicious Go modules and known-bad publisher accounts, so that a module later confirmed malicious gets flagged in your existing builds, not just blocked at the next fresh install. That closes the exact window that made the tlsproxy-style incidents damaging: the gap between takedown and actual removal from every consumer's build cache.

Combined with policy gates in CI/CD, SBOM generation for every Go build, and alerting the moment a suspicious module enters a dependency graph, Safeguard gives engineering teams the pre-publication scrutiny that pkg.go.dev itself doesn't provide — turning a decentralized, trust-on-first-use ecosystem into one where every dependency has actually been checked before it ships.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.