Go Toolchain Vulnerabilities: Your Go Version Is a Dependency
Most Go dependency scanning stops at the modules listed in go.mod. That misses two packages every Go build depends on: the standard library and the toolchain that compiles it. Go toolchain vulnerabilities, flaws in the Go compiler and the go command, apply at the Go version a module builds with. So do standard library vulnerabilities. Your Go version is a dependency, and it needs the same scanning and upgrade discipline as any module.
The second gap is in how fixes land. A vulnerable Go module is often not a direct dependency. It shows up as an // indirect line in go.mod, or it is swapped in through a replace directive. The fix for each of those is a different edit, and the usual advice, "upgrade the direct dependency", is often not the smallest one.
This post covers what to scan in a Go module, how the go and toolchain directives decide which version that is, and how to fix vulnerable modules that sit behind indirect requirements and replace directives.
Why the Go version is a dependency
A Go binary carries the code of every module it imports. It also carries the standard library code it uses, compiled from the Go release that built it. A flaw in net/http, crypto/tls or archive/zip lives in your binary exactly like a flaw in a third-party module. There is no package to bump in go.mod. The fix is a newer Go release and a rebuild.
The toolchain is a separate case. The compiler, linker and go command do not ship inside your binary, but they run on your developer machines and CI runners, and they process code you did not write. Past Go security releases have fixed issues in how the go command handles module fetching, version control metadata and cgo build flags. A flaw there can mean code execution at build time, or a build that does not produce what the source says it should.
That gives a Go module three layers to scan:
- Modules in the module graph, direct and indirect.
- The standard library at the Go version the module builds with.
- The toolchain, the compiler and the
gocommand, at that same version.
Treat the last two as always present. Every Go program imports something from the standard library, and every build runs the toolchain. "Is this package used?" is not a useful question for either.
The go and toolchain directives in go.mod
Two lines in go.mod describe the Go version. They mean different things, and it matters which one you read.
module example.com/payments
go 1.25.0
toolchain go1.25.4
go 1.25.0is the minimum Go version the module requires. Since Go 1.21, thegocommand refuses to build a module with an older toolchain than this line declares.toolchain go1.25.4is the toolchain the module prefers when the local one is older. With the defaultGOTOOLCHAINsetting, thegocommand can download and switch to that version.
Neither line proves what built a given binary. A CI image can carry a newer Go than either line. A developer can set GOTOOLCHAIN=local and build with whatever is installed. A container base image pinned two years ago can keep compiling with an old release long after go.mod moved on.
The go line is still the right baseline for scanning source. It is the floor the module declares, and it is the version every consumer of the module must at least run. If the floor is a release with known vulnerabilities, the module allows a vulnerable build. Raising it is how you make the fix stick.
Bare versions and patch levels
A go line written as go 1.25, with no patch number, says nothing about which patch release is in use. A careful scanner reads it as the lowest release in that line, 1.25.0, because that is the oldest Go the module permits. Every security fix released in a later 1.25 patch counts as missing until the line says otherwise.
The practical rule: write the full patch version on the go line once you have a fixed release, and keep the toolchain line at or above it.
What built the binary
For shipped artifacts, check the binary rather than the source. A Go binary records the Go version that built it:
$ go version ./bin/payments
./bin/payments: go1.24.2
If that version is behind what go.mod declares, the gap is in your build environment: the CI image, the Dockerfile FROM golang: tag, or a pinned setup step. Fix the build environment, then rebuild and redeploy. A stdlib fix only reaches production when the binary is rebuilt with the fixed release.
How to fix Go standard library and toolchain vulnerabilities
Go publishes security fixes as patch releases on the two supported release lines. The fix for a stdlib or toolchain advisory is almost always "move to the patch release that contains it."
- Find the fixed release. The advisory names the first patched version in each supported line.
- Raise the floor. Set the
goline to at least that release, and thetoolchainline to match or exceed it. - Update the build environment. CI setup steps, Dockerfiles and developer tooling must use the new release. A version file nobody reads does not patch anything.
- Rebuild and redeploy. Confirm with
go versionon the new binary.
// before
go 1.24
// after
go 1.24.9
toolchain go1.24.9
Raising the go line has a cost for library authors. Every module that imports yours must then build with at least that version. For an application module, that cost rarely matters. For a widely imported library, raising the floor too far forces it on every consumer. Many library maintainers raise the toolchain line for their own builds and leave the go line lower. Applications should raise both.
A Go release line that has reached end of life stops receiving security patches. A module still on it collects stdlib and toolchain findings that no patch release will fix. The fix there is a minor version upgrade, which can bring language and API changes, so plan it as real work rather than a patch bump.
How Go dependency fixes land in go.mod
Since Go 1.17, go.mod lists every module the main module needs to build, not just the direct ones. Modules your code does not import directly are marked // indirect. Go's minimal version selection then builds with the highest version any requirement asks for.
That has a useful consequence. A vulnerable module listed as an indirect requirement can often be fixed by raising that one line.
Raise the indirect requirement
require (
github.com/acme/router v1.8.2
github.com/acme/httpclient v0.23.0 // indirect
)
If github.com/acme/httpclient needs to be at v0.24.1 or later, raising its indirect line is enough:
require (
github.com/acme/router v1.8.2
github.com/acme/httpclient v0.24.1 // indirect
)
The direct dependency stays where it is. The diff is one line, and the build selects the fixed version. On the command line, go get github.com/acme/httpclient@v0.24.1 followed by go mod tidy produces the same edit.
Two cautions. First, the fixed version can itself require newer versions of other modules. go get raises those too, so the real diff can be several lines. Review all of them, not just the one you asked for. Second, the parent module was tested against the old version. A raise is usually safe within a major version, but run the tests.
The alternative, upgrading the direct dependency that pulls the module in, is the right call when a newer parent release already requires the fixed version and you want the parent upgrade anyway. It is a bigger change, and it can fail to resolve the issue if no parent release has caught up yet. Prefer the smallest edit that fixes the vulnerability. The same trade-off shows up in npm, which we covered in overrides vs direct upgrades.
Move the replace target
A replace directive swaps one module for another version, a fork, or a local path. It wins over every require line for that module. Raising the requirement does nothing while the replace still points at the vulnerable version.
replace github.com/acme/yaml => github.com/acme/yaml v1.4.0
The fix is to move the replace target:
replace github.com/acme/yaml => github.com/acme/yaml v1.4.3
Before you move it, find out why the replace exists. A replace often pins around a breaking change or a bug, and the reason is rarely written down. If the replace points at a fork, the fix has to land in the fork first. If the reason no longer holds, removing the replace and letting minimal version selection pick the version is often the cleaner fix.
Replace directives apply only in the main module. A replace in a library's go.mod has no effect on the applications that import it. When you scan an application, read its own replace directives, not those of its dependencies.
What to scan and how to fix: a Go checklist
- Scan the Go version, not just the modules. Report stdlib and toolchain vulnerabilities at the module's
goline, and treat both as always present. - Write full patch versions. Replace bare
go 1.xxlines with the patched release once you have one. - Keep the toolchain line at or above the go line in application modules.
- Check the build environment. CI images, Dockerfile base tags and setup steps must match what go.mod declares.
- Check shipped binaries with
go versionorgo version -m, and rebuild after every stdlib fix. - Track end of life. A module on an unsupported Go line needs a planned minor upgrade.
- Fix indirect modules at the indirect line when that is the smallest working edit, and review the extra modules the raise brings along.
- Check for a replace first. If the vulnerable module is replaced, move the replace target or remove the replace.
- Run the call-graph check with
govulncheckfor third-party modules, so effort goes to modules whose vulnerable functions your code reaches.
How Heeler handles Go toolchain vulnerabilities and go.mod fixes
In Heeler SCA, a Go module carries two extra packages beside its modules. Vulnerabilities in the Go standard library appear on the package stdlib. Vulnerabilities in the Go compiler and the go command appear on the package toolchain. Both are reported at the module's Go version, and a bare minor version such as go 1.26 is read as 1.26.0. The findings carry version data and upgrade advice from Go's releases.
Heeler treats stdlib and toolchain as always present and reachable. They carry no reachability verdict and are never offered for removal. End-of-life status for the Go version appears on stdlib.
For vulnerable modules, a remediation lists the candidate solutions by the kind of change they make. Two are specific to go.mod:
- go.mod Requirement raises the module's indirect requirement line in go.mod. No direct dependency changes. When go.mod lists the module as an indirect requirement, this solution comes before the parent-module upgrade. When the raise needs other modules to move as well, the solution lists those additional upgrades, and the fix applies them together.
- go.mod Replace changes the target version of the replace directive that substitutes the module.
Fix Now and PR guardrail Auto-fix apply both edits. With SCA Auto-fix, a Fix Now on a requirement raise applies the additional module upgrades in the same pull request. In a guardrail Auto-fix, when one of those additional upgrades cannot be applied automatically, the violation goes to manual review.
FAQ
What are Go toolchain vulnerabilities?
Go toolchain vulnerabilities are security flaws in the Go compiler, linker or go command. They are fixed in Go patch releases, and they apply to any module that builds with an affected Go version.
Which go.mod line decides the Go version to scan?
The go line is the right baseline, because it is the minimum version the module allows. The toolchain line is the version the module prefers, and the binary itself records what actually built it.
Does upgrading a module fix a standard library vulnerability?
No. Standard library code comes from the Go release that compiles the binary, so the fix is a newer Go release and a rebuild.
Should I raise an indirect requirement or upgrade the direct dependency?
Raise the indirect requirement when it is the smallest edit that selects the fixed version. Upgrade the direct dependency when a newer parent release already requires the fix and you want that upgrade anyway.
Why didn't raising the requirement fix the module?
A replace directive for that module overrides every requirement line. Move the replace target to the fixed version, or remove the replace if it is no longer needed.
Go modules have three layers to secure: the module graph, the standard library and the toolchain that builds it. Heeler reports all three for each Go module, and fixes vulnerable modules with the smallest go.mod edit that works, whether that is an indirect requirement raise or a replace target move. See it on your own repositories. Get a demo


.jpg)
