Go’s official blog introduction has their mitigation measures for supply chain attacks. Go’s toolchain and design are said to include attack risk mitigation considerations at all stages.
All builds are “locked”
External changes (such as releasing a new version of a dependency) do not affect Go builds.
Unlike the configuration files used by most other package managers, Go modules do not have a separate list of constraints and lock files for locking specific versions. The version of each dependency participating in a Go build is determined entirely by the go.mod file of the main module.
As of Go 1.16, the above operation is performed by default, and the build command will fail if go.mod is incomplete. The only commands that will change go.mod
are go get
and go mod tidy
. These commands are not usually run automatically or in CI, so such changes to the dependency tree are usually deliberate and can be detected during the code review phase.
This is important for security; if a module is compromised and a new malicious version is released, no one will be affected until that dependency is explicitly updated, providing ecology with time to review the changes and detect the incident.
Module version content never changes
Another key property to ensure that third parties do not affect the build is that the contents of the module version are immutable . This is because if an attacker who breaks a dependency can re-upload an existing version, they can automatically break all projects that depend on that dependency.
This is exactly what the go.sum
file is for. It contains a cryptographic hash list of each dependency that helped build it. Again, an incomplete go.sum
will cause errors and can only be modified using go get
and go mod tidy
. Therefore, any modification to it will be accompanied by subjective dependency changes.
VCS is the de facto source
Most projects use a version control system (VCS) during development, and in other ecologies, these projects also require uploads to a central package repository (such as npm). This means that there are two accounts that can be compromised, the VCS host and the central package repository. The latter is much less used and more likely to be ignored. It also means that it is easier to hide malicious code in versions uploaded to the repository, especially if the source code is routinely modified as part of the upload.
Go has no such thing as a central package repository account. The package import path embeds the information needed to get its modules directly from the VCS go mod download
, where the tag on the VSC defines the module version.
Build code only, but don’t execute it
The Go toolchain has an explicit security design goal: either fetch or build code, but not have that code execute, regardless of whether the code is untrusted or malicious.
This is a meaningful risk mitigation measure if you are executing a binary or testing a dependency on a package module that uses only a subset. For example, if example.com/cmd/devtoolx
is built and executed on macOS, then a dependency for Windows or a dependency for example.com/cmd/othertool
is unlikely to compromise your machine.
In Go, modules that do not provide code for a particular build have no security implications for the build.
“Replication trumps dependencies”
The last (and probably most important) supply chain attack risk mitigation measure is also the least technical: Go has a culture of rejecting large dependency trees and prefers copying to adding new dependencies .
This can be traced back to a Go adage: “a little copying is better than a little dependency”.
Go modules are very proud of their “zero dependency” label. If a developer needs to use a library, he will find that it does not make him dependent on dozens of modules from other authors and owners.
This means that rich, complex applications can be built with only a few dependencies. After all, no matter how good a tool is, it cannot eliminate the risks involved in reusing code, so the strongest mitigation is always only a small dependency tree.