Unveiling 'poutine': An Open Source Build Pipelines security scanner
TL;DR BoostSecurity.io is thrilled to announce ‘poutine’ – an Open Source security scanner CLI you...
TL;DR: Malicious code caching, dangling commits, pseudo-versions stealthily pointing to backdoors... Go makes you just as vulnerable as other ecosystems to social engineering attacks, and can even help malicious actors cover their tracks. Go enables new manipulation techniques to subtly trick users into downloading malicious packages. In this article, we describe various attack vectors in the Go ecosystem, from social engineering to well-known attacks such as repojacking, domain hijacking, and dependency confusion. Go's ecosystem guarantees integrity, not trust.
December 3rd 2025, by Garance de la Brosse
The Go language, first conceived at Google in 2007, was designed with security and simplicity as core principles. A key part of its design is a decentralized package ecosystem. Unlike package managers that rely on a single, central repository, Go identifies packages by their full path, which is made up of the domain name hosting the package and its name. This decentralized nature means any server is a potential package host, so long as it serves the correct go-get meta tags.
To secure this system, the Go toolchain relies on two main pillars by default: the Go Proxy (https://proxy.golang.org) and the Go Checksum Database (https://sum.golang.org), both managed by Google [d-2-4].
The Go Proxy caches and redistributes compressed packages containing the Go source code files. This cache ensures immutability; packages, once stored, are available forever and cannot be altered or deleted. The Go Checksum Database complements this by providing a global, append-only log of cryptographic hashes to guarantee package integrity.
However, a critical distinction must be made: these mechanisms ensure a package's integrity, not its trust. The checksum database guarantees that a downloaded package version is identical to the one everyone else is using; it does not ensure the package is safe to use.
While this model is demonstrably more secure by design than repositories like PyPI or NPM, it has dark sides that attackers exploit…
Unlike npm or pip, the go CLI is designed not to execute scripts during installation (for modern Go version post 1.18). Even though implementation bugs have led to malicious code being executed during installation in the past (see CVE-2023-39320 and CVE-2025-4674), go get is designed to be safe to use.
But if a malicious dependency was downloaded, running go build, go run, go test, or go install commands should be sufficient to execute the malicious script. Indeed, if the malicious dependency is imported and used in the code, running one of those commands will automatically initialize its global variables, which can contain function calls to malicious functions, and call its init() functions.
History has shown that attackers do use Go as a language for writing malware. Advanced Persistent Threats (APTs) have used it for backdoors, such as the Sunshuttle malware connected to the SolarWinds attack [1]. Palo Alto Networks research (July 2019 [2]) analyzed roughly 10,700 unique malicious Go samples, encompassing 53 unique malware families.
This trend poses a critical question: does the Go ecosystem design, often praised for its security, actually protect developers from ingesting these malicious packages as dependencies? In this article, we will study the specific attack vectors that can lead a user to download a malicious Go package.
If a package is hosted on a shared platform such as GitHub, it is identified by the global domain name, the unique username or organization name, and the package name: github.com/username/package.
If a legitimate owner were to change their username, rename their organization, or delete their account, this would free up the original namespace, which the attacker could then claim. This would lead to an attack called “repojacking”, in which an attacker would take control of an account, and therefore a Go package, giving them ownership. The attacker would then be able to publish new malicious versions of a legitimate package simply by registering a new GitHub account.
GitHub's protection rule - removal of namespaces with more than 100 clones/uses by actions in the week prior to their deletion [d-1] - is often insufficient to prevent repojacking, as even very popular repositories may not reach this cloning threshold. Notably, popular Go packages may be cloned only once in the Go Proxy, but downloaded many times from the Go Proxy, adding no additional clones count to the package.
A study from Vulncheck from December 2023 [3] found more than 15,000 GitHub repositories supporting more than 800,000 Go module-versions vulnerable to repojacking. This is scary.
Another study - MavenGate from Oversecure [14] - published on January 17, 2024, highlighted more than 6 170 hijackable maven projects impacting 200 companies, among which high-profile ones.
To update and complete those studies on the Go ecosystem, we retrieved the entirety of the Go Proxy index to identify GitHub repositories that were currently vulnerable to repojacking. We found MANY.
We ran the analysis in early November 2025 and we found a total of 34845 deleted GitHub accounts among which 24339 GitHub accounts transferred their repositories to another account before deleting their accounts, leading us to believe that the projects are intended to still be used today. Those repojackable GitHub accounts were hosting a total of 63386 potentially hijackable Go packages still cached in the Go Proxy.
This alarming number had us digging to see if this list contained any popular packages. Since the Go Proxy does not share stats about Go packages usage, we had to craft measurements to determine a package's popularity.
We first mapped our list of hijackable GitHub repositories to the list provided by github.com/uhub/awesome-go. The intersection returned 3 go packages owned by 3 deleted GitHub accounts, among which 2 are registrable.
To go further, we created a list of popular go packages on GitHub by extracting open source packages from the OSSF BigQuery database that have a Criticality Score >= 0.2 ([4]). Although this threshold may seem low, with the criticality score capped at 1, we justify its choice in particular with the 2022 OpenSSF article [5] calculating the criticality scores of GitHub repositories for more than 1,300 Census II packages: a list aimed at establishing the “most commonly used free and open source software components in production applications” [6]. The OpenSSF analysis shows us that widely used Go packages can have a score as low as approximately 0.05, with the first quartile having a score between this minimum and approximately 0.45. We therefore found it reasonable to choose a threshold of 0.2, which includes the upper half of the first quartile. We found 51 packages belonging to 50 deleted GitHub accounts, on which the open source community depends, that have this significant criticality score. That's a total of 54 popular GitHub packages that are potentially vulnerable to hijacking and can be found in the Go Proxy's index.
An example of our findings is the popular flower-corp/rosedb package, which has reached 5k stars on GitHub. Fortunately, even though the GitHub account is registrable, the name of the rosedb repository is not. But this is a significant advantage for brandjacking attacks.
Just to scare ourselves a little more, we decided to extract every single Go package hosted on an expired account that was still being used in at least one public GitHub project. Querying the GitHub API for this took a very long time, but the result was worth the wait: a frightening 8 452 hijackable repositories are currently sitting in public go.mod files. We also cross-referenced this massive list against pkg.go.dev to check their "Imported By" counts, another lengthy process. We found 1 602 zombie packages that are still actively imported by at least one other Go project in the ecosystem. The intersection of these datasets reveals a staggering total of nearly 9 571 hijackable GitHub repositories that are currently dependencies for public Go projects. The most critical findings? We identified packages that remain integral to the Go ecosystem, with some serving as dependencies for up to 6.3k public GitHub projects. While the four most critical packages were fortunately non-registrable, the top exploitable (registrable) package still had 124 public GitHub projects depending on it and possessed 1.2k stars on GitHub. And it's simply impossible to know for sure how widely those downstream projects are used.
If an attacker were to claim one of these abandoned names, they could effortlessly publish a malicious tag, waiting to be ingested by a simple go get command. Given these alarming statistics, are you sure you don't depend on one of them...?
In order to detect these GitHub dependencies vulnerable to repojacking, we developed a tool in Go that we named Gobelin, which has already been released as open source. This tool calculates the SBOM of a Go project from go.mod, then for each GitHub account responsible for a project dependency, it queries the GitHub API to verify that the account is still registered—and therefore not repojackable.
This attack vector applies to any code hosting service that allows accounts and organizations to be renamed and deleted, so certainly not limited to GitHub.
A similar risk exists for packages hosted on a domain owned by an organization or a private domain (e.g., mycompany.com/internal/pkg). This domain cannot be a widely shared one like github.com or gitlab.com; it must be a customized domain owned by the company (unless you feel like taking over github.com). If this custom domain expires, an attacker can take control of the package by being the first to register the expired domain. As in the case of repojacking, this would give them complete control over the package with minimal effort.
After dumping the Go Proxy database to get all the domains hosting Go packages downloaded at least once (we cannot know to what extent they are used), we found 6 expired domains, among which 2 are registrable, 3 are in Redemption Period for at least 6 days and will be freed if not paid on time, and 1 is frozen. One of them, about to be deleted (status pendingDelete), has 13 Go modules that, fortunately, do not appear to be widely used: 18 uses in go.mod files.
We also found 13 domains about to expire in the next 15 days (as of November 18th 2025).
Thankfully the ones expired do not seem to be broadly used (go.mod file research on GitHub did not return much usage), and the only one widely used and critical project appears to be frozen. We can also suppose that the very popular ones about to expire will auto renew, given their critical importance. We’ll keep watching.
Dependency confusion is an attack that was first revealed by Alex Birsan [7]. The literature highlights the dangerous nature of this attack, particularly for centralized ecosystems such as NPM or PyPI.
Dependency confusion in decentralized ecosystems like Go is difficult, but not impossible. It requires more prerequisites than other ecosystems that are based only on package names.
Because Go identifies packages with a complete domain and package name, achieving dependency confusion requires owning a public domain that is a homonym of the private domain used for the targeted package. If a developer knows of such a private domain that is unclaimed, or is expiring soon, an attacker could take over this domain. The public domain would then be attacker-owned, creating a risk of dependency confusion.
To illustrate this attack, we registered a free domain and published a public package on it, called flourishing-cuchufli-ea1df0.netlify.app/lets-a-go. We also have a private domain hosting a go package with the same name. Both have a Hello() method that displays their identity.
A new developer is working on a Go project using a private package hosted in a domain from the internal network of their company. If this dev just decides to run go get from the root of the project, without thinking about setting GOPRIVATE to point to the private domain, then Go will download the public package from the Go Proxy.
If the go.mod specifies major versions only (eg. v1), then go will download the latest minor version available and add it to the go.sum without raising any error. (Fig. 1)
$ cat go.mod
module test
go 1.25.1
require flourishing-cuchufli-ea1df0.netlify.app/lets-a-go v1
$ go get
$ go run .
Hello from a very public domain!
Fig. 1: Download the malicious public version
If the go.mod specifies the minor version of the module (eg. v1.0.2), go will try to fetch this exact version and compare its hash to the one stored in the go.sum. A hash conflict will raise a security error thanks to the go.sum. (Fig. 2)
$ cat go.mod
module test
go 1.25.1
require flourishing-cuchufli-ea1df0.netlify.app/lets-a-go v1.0.2
$ go get
go: downloading flourishing-cuchufli-ea1df0.netlify.app/lets-a-go v1.0.2
verifying flourishing-cuchufli-ea1df0.netlify.app/lets-a-go@v1.0.2: checksum mismatch
downloaded: h1:cvjmB212IbbgE/RM0w***
go.sum: h1:DsjPXjP0KwW6veP***
SECURITY ERROR
Fig. 2: go.sum mismatch running go get
However, if the developer explicitly downloads the missing package (which caused the crash) with go get domain.com/package, then Go will fetch the latest available minor version of this package from the Go Proxy, ignoring the version required in the go.mod. If this version is higher than the highest minor version of the internal package (more precisely higher than the highest minor version whose hash is stored in the go.sum), then the hash of the version will not be in the go.sum. Go will successfully download the new version, add its hash to the go.sum, and the developer will be free to execute the malicious version by running go run . . (Fig. 3)
$ go get flourishing-cuchufli-ea1df0.netlify.app/lets-a-go
go: downloading flourishing-cuchufli-ea1df0.netlify.app/lets-a-go v1.0.3
go: upgraded flourishing-cuchufli-ea1df0.netlify.app/lets-a-go v1.0.2 => v1.0.3
$ go run .
Hello from a very public domain!
Fig. 3: Download the malicious package and add it to go.sum
Even if these developers are well aware of the existence of internal packages in the project and know that they MUST define GOPRIVATE variables, and even if no developer forgets to do so, what happens if a remote worker forgets to connect to the company's internal network before running go get? Then GOPRIVATE will inadvertently help them to download the malicious package: Go will download the package directly from the VCS without going through the Go Proxy and without checking any hash from the Go sum proxy. The new malicious package will be downloaded, its hash added to the go.sum, and it will be executed once the dev runs go run ..
Even with GOPRIVATE set, a lot can go wrong if the internal network is not securely configured and falls back to the internet, or if the private domain changes name without EVERYONE applying the correction. If developers rely on a third-party proxy to manage internal and public Go dependencies, the risk is delegated to that proxy.
In summary, the Go ecosystem is much less susceptible to dependency confusion attacks, but it is still not invulnerable. It is therefore necessary to remain cautious and configure your development environments securely.
Typosquatting is a deceptive attack where malicious actors register package names or paths that look nearly identical to popular legitimate ones—such as using a slightly different username or a confusing prefix—hoping developers unknowingly import the counterfeit version.
During our investigation of the Go Proxy, we found a package called v4n5haj/discord-mass-dm-go, typosquatting the popular package V4NSH4J/discord-mass-DM-GO. The counterfeit package is stored in the Go Proxy cache and is therefore available for download, but the GitHub account has been deleted in order to cover its tracks. We couldn't find any statistics on pkg.go.dev, and fortunately no public projects on GitHub seem to be using it, according to our research on GitHub.
This type of attack in Go can go even further, as some unofficial services allow for more sophisticated and stealthy typosquatting:
gopkg.in is a service acting as a proxy that allows the go tool to work with older GitHub versioning styles (using branches like v1 instead of tags).
To access a package hosted at github.com/username/pkg, the path would be gopkg.in/username/pkg.v*. But if the GitHub username or organization starts with 'go-' (e.g., the famous github.com/go-yaml/yaml), the path is shortened, omitting the username: gopkg.in/yaml.v*.
This creates a typosquatting vector. An attacker can target a popular package by registering a confusingly similar username that uses the special go- prefix. A developer might mistakenly import the attacker's package using the shortened path.
Furthermore, a significant systemic risk exists: the gopkg.in domain, which is a dependency for many projects, is set to expire on March 5, 2026. If the owner, who appears to be solo (only 3 other contributors), fails to renew it, an attacker could take over the domain, compromising every build that depends on it (326k on GitHub). Our concern is heightened by the fact that the project no longer appears to be maintained, with the last commit dating back to 18 October 2023. We know that high-profile projects depend on gopkg.in, such as Kubernetes and Reddit, for example.
Go's replace directive forces the substitution of one module for another across the entire project. This is controlled by adding directives to a root go.mod file or, in a multi-module workspace, to the go.work file.
When a module is replaced in the go.mod file, the go mod graph command does not return the replacement (the new module), but the original one. The graph does not reflect the dependencies actually used by the project in the event of a module replacement (Fig.4).
$ cat go.mod
module my-project
go 1.25.1
replace github.com/coderyoo1/let-is-go => flourishing-cuchufli-ea1df0.netlify.app/lets-a-go v1.0.2
require github.com/coderyoo1/let-is-go v0.0.1 !
$ go get
go: downloading flourishing-cuchufli-ea1df0.netlify.app/lets-a-go v1.0.2
$ go run .
Hello from a very public domain!
$ go mod graph
my-project github.com/coderyoo1/let-is-go@v0.0.1
my-project go@1.25.1
github.com/coderyoo1/let-is-go@v0.0.1 go@1.25.1
go@1.25.1 toolchain@go1.25.1
Fig. 4: go mod graph - no module replacement
We can see that the replaced module is downloaded, but the graph shows the unused original module.
However, the SBOM (such as one generated by go list -m all or cyclonedx-gomod) will correctly display the replacement.
The package downloaded to the cache is stored under the name of the package used, not under the name of the package replaced.
A malicious actor with write access could add a replace directive to a project's work file or root go.mod file, burying it among other changes, and thus replace the dependency with a malicious one. This would require a certain degree of social engineering.
Because the Go Proxy caches module versions forever, an attacker can take advantage of this immutability to create a discrepancy between the code in the proxy and the code visible on GitHub.
An attacker with write access to a repository can exploit the mutability of Git tags:
Because the Go Proxy ensures immutability, the tag will not be updated in the proxy. The malicious version remains cached forever, creating a gap between the version in the Go Proxy and the clean version now appearing on GitHub. This technique allowed the malicious boltdb-go/bolt typosquat to remain undetected for over three years [8].
If an attacker has write access to a branch (even a non-default branch), they can use pseudo-versions for a stealthier attack. Pseudo-versions allow Go to download a specific commit hash, whether it has a tag or not.
The commit is now "dangling", is no longer visible when browsing through the different branches on GitHub, accessible only via its hash [Fig. 5], but it remains permanently in the Go proxy.

Fig. 5: Dangling commit
If the attacker successfully adds the dangling to the go.mod, then the next developer using the project will download the dangling commit (Fig. 6).
$ cat go.mod
module test
go 1.25.1
require flourishing-cuchufli-ea1df0.netlify.app/lets-a-go v1.0.4-0.20251106114319-b6dc20436cc6
$ go get
go: downloading flourishing-cuchufli-ea1df0.netlify.app/lets-a-go v1.0.4-0.20251106114319-b6dc20436cc6
$ go run .
Hello from a dangling commit!
Fig. 6: Downloading pseudo-version pointing to a dangling commit
The described attacks require social engineering: the attacker submits a pull request with a seemingly minor "dependency fix." The change to the go.mod file, either in a require or replace directive, points to this malicious pseudo-version. It is unlikely that maintainers will spot the malicious hash, as it's lost in the noise of many other legitimate updates.
Fortunately, a key limitation exists: the attacker needs write access to the imported project. Go's tooling (and the proxy) will not fetch commits that do not belong to the repository's advertised branches, such as commits that only exist in a pull request from a fork. The dangling commit must have been on a real, pushed branch (even temporarily) to be cached.
What if the attacker could use pseudo-versions that point to their malicious code, that would appear to belong to legitimate projects recognized by the community? And this without having write access to these projects?
If the attacker were able to perform a Bot-Delegated TOCTOU attack, as described in François Proulx's article [10], they would be able to trick a bot into promoting their code from a fork of the project, to a branch in the target repository. The malicious code would then be accessible from the legitimate repository, allowing the malicious pseudo-version to be discreetly imported into a go.mod, presented as coming from a trusted package.
Once the pseudo-version is cached in the Go Proxy, it does not matter if it is removed from the legitimate repository, it will forever be accessible using the legitimate project name and a hash no one will know about.
The Go ecosystem is often praised for its security by design, but our research proves that integrity is not the same as trust. While the Go Proxy and Checksum database guarantee that a package has not been modified, it cannot guarantee that the package is safe to use.
Our analysis of the ecosystem reveals a massive blind spot. We identified 63,386 packages hosted on deleted GitHub accounts that are still cached and available in the Go Proxy. Among these, we found 9,571 of these hijackable "zombie" repositories are currently being imported as dependencies in public Go projects.
We also demonstrated that the Proxy’s greatest feature, immutability, can be weaponized! Attackers can cache malicious dangling commits that persist in the Proxy forever, even after being scrubbed from the source repository to cover their tracks.
From repojacking to dependency confusion and permanent malicious caches, the attack surface is wider than previously thought. The tools are secure, but the chain of trust is broken. Are you sure you know who owns your dependencies today?
[1] CISA, “MAR-10327841-1.v1 – SUNSHUTTLE”, April 15, 2021
https://www.cisa.gov/news-events/analysis-reports/ar21-105a
[2] Josh Grunzweig, “The Gopher in the Room: Analysis of GoLang Malware in the Wild”, July 1, 2019
https://unit42.paloaltonetworks.com/the-gopher-in-the-room-analysis-of-golang-malware-in-the-wild/
[3] Jacob Baines, “Hijackable Go Module Repositories”, December 4, 2023
https://www.vulncheck.com/blog/go-repojacking
[4] OpenSSF, “Criticality Score”, accessed in November 2025
https://openssf.org/projects/criticality-score/
[5] Henrik Plate, “Apples and apples? Comparing Approaches to Measuring Criticality and Risk at the OpenSSF”, December 8, 2022
[6] Jason Perlow, “A Summary of Census II: Open Source Software Application Libraries the World Depends On”, 07 March 2022
[7] Alex Birsan, “Dependency Confusion: How I Hacked Into Apple, Microsoft and Dozens of Other Companies, Feb 9, 2021
https://medium.com/@alex.birsan/dependency-confusion-4a5d60fec610
[8] Socket.dev, “Go Supply Chain Attack: Malicious Package Exploits Go Module Proxy Caching for Persistence, Go Supply Chain Attack: Malicious Package Exploits Go Module Proxy Caching for Persistence”, February 4, 2025
https://socket.dev/blog/malicious-package-exploits-go-module-proxy-caching-for-persistence
[9] Michael Henriksen, “GitLab catches MongoDB Go module supply chain attack”, June 30, 2025
https://about.gitlab.com/blog/gitlab-catches-mongodb-go-module-supply-chain-attack/
[10] François Proulx, “Split-Second Side Doors: How Bot-Delegated TOCTOU Breaks The CI/CD Threat Model”, November 12th 2025
[11] Kevin Backhouse, “How to stay safe from repo-jacking”, February 21, 2024
https://github.blog/security/supply-chain-security/how-to-stay-safe-from-repo-jacking/
[12] Benedikt, “How to Tamper with Releases built with GitHub Actions Workflows”, September 2025
https://github.com/offensive-actions/release-tampering-pocs
[13] Carmine Cesarano, Vivi Andersson, Roberto Natella, Martin Monperrus, “GoSurf: Identifying Software Supply Chain Attack Vectors in Go”, 05 Jul 2024 https://arxiv.org/html/2407.04442v1
[14] Oversecured, January 17, 2024, Introducing MavenGate: a supply chain attack method for Java and Android applications
https://blog.oversecured.com/Introducing-MavenGate-a-supply-chain-attack-method-for-Java-and-Android-applications
[d-1] Github documentation: Renaming an organization, accessed in November 2025
https://docs.github.com/en/organizations/managing-organization-settings/renaming-an-organization
[d-2] Go documentation, accessed in November 2025
[d-2-1] Developing and publishing modules
https://go.dev/doc/modules/developing
[d-2-2] Effective Go
https://go.dev/doc/effective_go
[d-2-3] Go Modules Reference
[d-2-4] Go Module Mirror, Index, and Checksum Database
[d-2-5] Using Go Modules
https://go.dev/blog/using-go-modules
[d-3] SLSA - Threats & mitigations, accessed in November 2025
TL;DR BoostSecurity.io is thrilled to announce ‘poutine’ – an Open Source security scanner CLI you...
TL;DR: We disclosed to Chainguard in December 2023 that one of their GitHub Actions workflow was...