Post

Golang - Patching

A practical guide to patching vulnerabilities in Go modules, covering different scenarios and utilizing Go-native tooling like govulncheck.

Golang - Patching

Vulnerability Management

Paraphrasing a little bit about the concept OWASP provides, vulnerability management is a cyclic process of controlling risk by identifying, prioritizing, fixing and monitoring weaknesses in Software Products.

This article will focus on remediation and mitigation. My motivation to write this article comes from the need to develop a practical example to show some common strategies and ways to tackle different situations during the fixing/remediation stage in this case inside the Go-lang environment.

For this purpose I created a repository with common scenarios to practice patching strategies. First clone this repo, and follow the instructions below. https://github.com/Tato418/go-vuln-lab

Happy hacking :)

The lab

This lab covers Software Composition Analysis (SCA) only — i.e. vulnerabilities in third-party Go modules. It uses Go-native tooling only:

Tool Role
govulncheck Call-graph aware vulnerability scanner backed by vuln.go.dev
go list -m -u all Reports modules with available upgrades
go mod graph / go mod why -m Dependency graph + reverse lookup
go mod verify Checksum integrity verification
go mod tidy Module graph normalization via Minimum Version Selection (MVS)

Scenarios

Slug Pattern Key technique
01-direct-cve-upgrade Direct dep with known CVE go get pkg@fixed + import path migration
02-transitive-cve-mvs Indirect dep vulnerable Top-level require forces MVS upgrade
03-callgraph-triage Vuln exists but unreachable Read govulncheck reachability output
04-replace-with-fork Upstream unmaintained replace directive + local fork
05-stdlib-toolchain-bump Stdlib CVE Bump go directive in go.mod
1
2
3
4
5
6
7
8
9
10
11
12
13
go-vuln-lab/
├── README.md
├── Makefile            # Lab-wide targets
├── tools/              # Install script + shared shell lib
├── scripts/            # Reusable scan / diff / update scripts
├── docs/               # Methodology, decision matrix, tool reference
└── scenarios/
    └── NN-<slug>/
        ├── vuln/       # Vulnerable module (run govulncheck here)
        ├── fixed/      # Patched module (run govulncheck here)
        ├── tests/      # Exploit test, fails on vuln/ passes on fixed/
        ├── Makefile    # scan / test / swap
        └── GUIDE.md    # Walkthrough

Scenario 01 - Simple pkg upgrade

The first scenario shows a simple upgrade dependency scenario. First enter de vuln folder and execute govulncheck ./...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
govulncheck ./...
=== Symbol Results ===

Vulnerability #1: GO-2022-0956
    Excessive resource consumption in gopkg.in/yaml.v2
  More info: https://pkg.go.dev/vuln/GO-2022-0956
  Module: gopkg.in/yaml.v2
    Found in: gopkg.in/[email protected]
    Fixed in: gopkg.in/[email protected]
    Example traces found:
      #1: main.go:49:26: loadconfig.LoadConfig calls yaml.Unmarshal

Vulnerability #2: GO-2021-0061
    Denial of service in gopkg.in/yaml.v2
  More info: https://pkg.go.dev/vuln/GO-2021-0061
  Module: gopkg.in/yaml.v2
    Found in: gopkg.in/[email protected]
    Fixed in: gopkg.in/[email protected]
    Example traces found:
      #1: main.go:49:26: loadconfig.LoadConfig calls yaml.Unmarshal

Vulnerability #3: GO-2020-0036
    Excessive resource consumption in YAML parsing in gopkg.in/yaml.v2
  More info: https://pkg.go.dev/vuln/GO-2020-0036
  Module: gopkg.in/yaml.v2
    Found in: gopkg.in/[email protected]
    Fixed in: gopkg.in/[email protected]
    Example traces found:
      #1: main.go:49:26: loadconfig.LoadConfig calls yaml.Unmarshal

Your code is affected by 3 vulnerabilities from 1 module.
This scan also found 0 vulnerabilities in packages you import and 8
vulnerabilities in modules you require, but your code doesn't appear to call
these vulnerabilities.
Use '-show verbose' for more details.

The output shows the following vulnerabilities, in total 3:

1
2
3
4
5
6
7
8
9
10
11
*Vulnerability #1: GO-2022-0956*
*Excessive resource consumption in gopkg.in/yaml.v2*

*Vulnerability #2: GO-2021-0061*
*Denial of service in gopkg.in/yaml.v2*

*Vulnerability #3: GO-2020-0036*
*Excessive resource consumption in YAML parsing in gopkg.in/yaml.v2*

*Vulnerability #3: GO-2020-0036*
*Excessive resource consumption in YAML parsing in gopkg.in/yaml.v2*

So we have 3 vulnerabilities in one package which are fixed in version gopkg.in/[email protected]

Fix by upgrading the dependency

Since the vulnerabilities are present in only one package we can proceed to bump the version to the latest non vulnerable. Also, since the bump is from version 2.2.0 to 2.2.8 we should be able to upgrade without having breaking changes. However, always run the tests.

go get gopkg.in/[email protected]

1
2
3
4
5
6
7
8
9
10
➜ govulncheck ./...
=== Symbol Results ===

**No vulnerabilities found.**

Your code is affected by 0 vulnerabilities.
This scan also found 0 vulnerabilities in packages you import and 8
vulnerabilities in modules you require, but your code doesn't appear to call
these vulnerabilities.
Use '-show verbose' for more details.

Scenario 02 - Transitive CVE (MVS-driven fix)

The dependency is not ours — we don’t import it directly — but it’s still in go.mod as an // indirect entry and still ships in your binary. govulncheck walks the whole call graph including across module boundaries, so the finding surfaces even though your code only touches mylib.

The fix is the Minimum Version Selection (MVS) trick: add an explicit top-level require for the patched version, and MVS will pick it over the transitive dep’s older pin.

First, execute govulncheck:

1
2
3
4
5
Vulnerability #1: GO-2022-0956
    Module: gopkg.in/yaml.v2
      Found in: gopkg.in/[email protected]
    Example traces found:
      #1: main.go:15:30: app.main calls mylib.LoadConfig, which calls yaml.

Detect which pkg imports the library,

1
go mod why -m gopkg.in/yaml.v2
1
2
3
4
# gopkg.in/yaml.v2
example.com/app
github.com/example/mylib <---- Transitive dependency
gopkg.in/yaml.v2

Fix — the MVS override

Edit vuln/go.mod, line 7

1
2
-require gopkg.in/yaml.v2 v2.2.0 // indirect
+require gopkg.in/yaml.v2 v2.4.0 // indirect
1
2
3
4
5
6
7
8
9
module example.com/app

go 1.22

require github.com/example/mylib v0.0.0

require gopkg.in/yaml.v2 v2.2.8 // indirect

replace github.com/example/mylib => ../testdata/mylib

Then execute:

1
go mod tidy        # rewrites go.sum with v2.2.8 hashes

You do need to touch testdata/mylib/go.mod — the library still claims v2.2.0, but MVS resolves to the max(v2.2.0, v2.2.8) = v2.2.8, which is the patched line.

1
2
3
4
5
6
7
8
9
10
➜ govulncheck ./...
=== Symbol Results ===

No vulnerabilities found.

Your code is affected by 0 vulnerabilities.
This scan also found 0 vulnerabilities in packages you import and 8
vulnerabilities in modules you require, but your code doesn't appear to call
these vulnerabilities.
Use '-show verbose' for more details.

Scenario 03 - Callgraph triage

A vulnerable module is in your go.mod, but your code never calls any of the vulnerable functions. govulncheck does call-graph aware analysis and reports:

1
2
3
4
5
6
7
8
9
10
➜ govulncheck ./...
=== Symbol Results ===

No vulnerabilities found.

Your code is affected by 0 vulnerabilities.
This scan also found 3 vulnerabilities in packages you import and 11
vulnerabilities in modules you require, but your code doesn't appear to call
these vulnerabilities.
Use '-show verbose' for more details.

The way of solve this issue is defense in depth, even if exit is 0, upgrade the dependency is key so in a future, code change can’t accidentally introduce a reachable vuln.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
=== Symbol Results ===

No vulnerabilities found.

=== Package Results ===

Vulnerability #1: GO-2022-0956
    Excessive resource consumption in gopkg.in/yaml.v2
  More info: https://pkg.go.dev/vuln/GO-2022-0956
  Module: gopkg.in/yaml.v2
    Found in: gopkg.in/[email protected]
    Fixed in: gopkg.in/[email protected]

Vulnerability #2: GO-2021-0061
    Denial of service in gopkg.in/yaml.v2
  More info: https://pkg.go.dev/vuln/GO-2021-0061
  Module: gopkg.in/yaml.v2
    Found in: gopkg.in/[email protected]
    Fixed in: gopkg.in/[email protected]

Vulnerability #3: GO-2020-0036
    Excessive resource consumption in YAML parsing in gopkg.in/yaml.v2
  More info: https://pkg.go.dev/vuln/GO-2020-0036
  Module: gopkg.in/yaml.v2
    Found in: gopkg.in/[email protected]
    Fixed in: gopkg.in/[email protected]

Fix

1
2
3
4
5
6
module example.com/cfgfmt

go 1.22

require gopkg.in/yaml.v2 v2.2.8

Scenario 04 - Replace with a patched fork

When an upstream module ships a known-vulnerable dep and the maintainer is unresponsive (archived repo, no commits in years, no advisory response), the right move is often to maintain a fork and use the replace directive in your go.mod to point at it.

The replace directive also works against remote forks (e.g. replace github.com/foo/bar => github.com/yourorg/bar v1.2.3); here we use a local directory for reproducibility.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
➜ govulncheck ./...
=== Symbol Results ===

Vulnerability #1: GO-2022-0956
    Excessive resource consumption in gopkg.in/yaml.v2
  More info: https://pkg.go.dev/vuln/GO-2022-0956
  Module: gopkg.in/yaml.v2
    Found in: gopkg.in/[email protected]
    Fixed in: gopkg.in/[email protected]
    Example traces found:
      #1: main.go:15:33: app.main calls upstream.LoadConfig, which calls yaml.Unmarshal

Vulnerability #2: GO-2021-0061
    Denial of service in gopkg.in/yaml.v2
  More info: https://pkg.go.dev/vuln/GO-2021-0061
  Module: gopkg.in/yaml.v2
    Found in: gopkg.in/[email protected]
    Fixed in: gopkg.in/[email protected]
    Example traces found:
      #1: main.go:15:33: app.main calls upstream.LoadConfig, which calls yaml.Unmarshal

Vulnerability #3: GO-2020-0036
    Excessive resource consumption in YAML parsing in gopkg.in/yaml.v2
  More info: https://pkg.go.dev/vuln/GO-2020-0036
  Module: gopkg.in/yaml.v2
    Found in: gopkg.in/[email protected]
    Fixed in: gopkg.in/[email protected]
    Example traces found:
      #1: main.go:15:33: app.main calls upstream.LoadConfig, which calls yaml.Unmarshal

How replace works

replace is a per-module directive in go.mod that overrides the resolved source for a given module path. The replacement target can be:

Target form Meaning
../local/path Local directory (this scenario)
./subdir Local subdir
github.com/yourorg/bar v1.2.3 Another module, pinned to a version
github.com/yourorg/bar commit-sha Another module, pinned to a commit (most explicit)
github.com/yourorg/bar branch Another module, pinned to a branch head (avoid in prod)

Scenario 05 - Patching std library

Third-party deps are not the only attack surface. The Go standard library is versioned per toolchain, not per Go module. When a stdlib CVE is published, the fix ships in the next patch release of the toolchain — and your go.mod has to be updated to pick it up.

  • go 1.22.0 — language features + module graph compatibility
  • toolchain go1.22.10 — exact toolchain to use (downloaded automatically)

In Go 1.21+, the toolchain directive in go.mod lets you pin exactly which compiler / stdlib is used, and go will download it on demand. Older Go versions had only the go directive, which loosely selected the language level but used the host’s toolchain.

Running govulncheck gives us the following.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
=== Symbol Results ===

Vulnerability #1: GO-2026-4601
    Incorrect parsing of IPv6 host literals in net/url
  Standard library
    Found in: net/[email protected]
    Fixed in: net/[email protected]
    Example traces found:
      #1: main.go:34:30: pkiverify.LoadCert calls x509.ParseCertificate, which eventually calls url.Parse

Vulnerability #2: GO-2025-4011
    Parsing DER payload can cause memory exhaustion in encoding/asn1
  ...
    Found in: encoding/[email protected]
    Fixed in: encoding/[email protected]

Vulnerability #3: GO-2025-4010 ...
Vulnerability #4: GO-2025-4009 ...
... etc, ~6+ reachable stdlib CVEs

In this case fixing the vulnerability is straightforward, proceed to edit go mod and replace the go version with the patched version, then execute:

1
2
➜ go mod tidy
go: downloading go1.26.4 (linux/amd64)

Execute again to check if the vulnerability is fixed.

1
2
➜ govulncheck ./...
=== Symbol Results ===

Further reading

This post is licensed under CC BY 4.0 by the author.