原文Jakub Jarosz - 2024.11.02

在编写 Go 代码时,如何时刻考虑安全性?要在一篇简短的文章中回答这个问题似乎不太可能。因此,我们将把范围缩小到一些具体做法上。

这些实践如果持续应用,将有助于我们编写健壮、安全且高效的代码。

  • 我们如何获取 Go 安全公告的最新信息?
  • 我们如何保持 Go 代码的补丁和更新?
  • 我们如何针对安全性和稳健性测试 Go 代码?
  • 什么是 CVE,我们在哪里可以了解最常见的软件漏洞?

邮件列表

让我们从最显而易见的地方开始——Go 邮件列表。我们需要订阅它,以便直接从源头获取所有重要的安全信息。包含安全修复的所有发布版本都会通过 golang-announce@googlegroups.com 邮件列表进行公告。一旦我们订阅了该列表,就可以确保不会错过任何重要的公告。

保持 Go 版本的更新

第二步是确保项目中的 Go 版本保持最新。即使我们不使用最新的语言特性,更新 Go 版本也能让我们获得已发现漏洞的所有安全补丁。此外,新的 Go 版本还确保了与新依赖项的兼容性,从而保护我们的应用程序免受潜在的集成问题。

第三步是了解哪些安全问题和 CVE 在哪些 Go 发布版本中得到了修复。我们可以在 Go 发布历史的网站上查看这些信息,然后在项目的 go.mod 文件中更新到最新版本。

在升级到新版本的 Go 之后,我们应确保此操作不会引入兼容性和依赖性问题,特别是在使用第三方包时。这在处理大型项目时可能风险更大,因为这些项目可能有几十甚至上百个直接或间接的包依赖。

关键在于通过消除潜在的依赖性问题来控制风险。这些问题可能包括需要紧急重构现有代码以使其与新依赖项兼容。例如,包、API 或函数签名的变更都可能导致此类问题。

使用 Go 工具

在确认使用没有安全问题的 Go 版本后,我们就可以专注于项目源代码了。我们可以通过使用静态代码分析工具来开始评估代码质量和安全性。

vet

在安装和使用第三方分析工具之前,最好先使用 Go 自带的 go vet 命令。

我们可以使用 go vet 命令来分析 Go 代码。没有参数的 go vet 命令会默认运行所有允许的选项。该工具扫描源代码并报告潜在问题,问题包括代码语法错误和某些可能在程序执行期间引发问题的编程结构。

最常见的问题包括 goroutine 错误、未使用的变量以及代码库中不可达的区域。使用 go vet 命令的主要优势在于它是 Go 工具链的一部分。

在另一篇文章中,我们将深入探讨 vet 的详细信息。更多的文档和示例可以在 go vet 网站 上找到。

staticcheck

Staticcheck 是另一个静态代码分析工具。它是一个第三方 linter(代码检查工具),有助于发现错误并检测可能的性能问题。它还能强制执行 Go 语言的代码格式。Staticcheck 提供代码简化建议,解释发现的问题,并通过示例提出修正建议。

除了将 staticcheck 运行在 CI 流水线中,我们还可以将其作为独立的二进制文件安装在本地电脑上,进行本地代码扫描。让我们安装最新版本:

1
go install honnef.co/go/tools/cmd/staticcheck@latest

终端没有报错?如果是这样,我们就可以准备运行扫描了。不过首先,需要检查已安装的版本,以确保一切正常。

1
2
staticcheck --version
staticcheck 2024.1.1 (0.5.1)

go vet 类似,运行没有参数的 staticcheck 会默认调用所有代码检查器。这种方式与 UNIX 编程哲学的合理默认值相吻合,不强迫用户进行不必要的操作。

让我们看看这个工具能在 NGINX Agent GitHub 仓库中发现什么。首先,我们需要克隆它:

1
git clone git@github.com:nginx/agent.git

然后,我们可以从项目的根目录运行它:

1
➜ staticcheck ./...

片刻之后,就可以查看扫描结果了。我们可以将列出的示例分为三类:

  • 已弃用的包、方法或函数,例如:
1
2
3
4
...
src/core/metrics/sources/cpu.go:111:9: times.Total is deprecated: Total returns the total number of seconds in a CPUTimesStat Please do not use this internal function. (SA1019)
...
test/component/nginx-app-protect/monitoring/monitoring_test.go:15:8: "github.com/golang/protobuf/jsonpb" is deprecated: Use the "google.golang.org/protobuf/encoding/protojson" package instead. (SA1019)
  • 未使用的变量和字段,例如:
1
2
3
src/core/metrics/sources/nginx_plus.go:74:2: field endpoints is unused (U1000)
src/core/metrics/sources/nginx_plus.go:75:2: field streamEndpoints is unused (U1000)
src/core/metrics/sources/nginx_plus_test.go:94:2: var availableZones is unused (U1000)
  • 与代码质量相关的潜在问题,例如:
1
src/core/nginx.go:791:4: ineffective break statement. Did you mean to break out of the outer loop? (SA4011)

现在,我们可以开始分析这些问题了。由于本篇文章是介绍性文章,深入分析代码库超出了其范围。在后续的文章中,我们将深入分析代码,展示示例,并修复安全性和性能问题。

目前,我们可以先记下 CWE 网站,它包含大量关于这些弱点的信息,供我们以后学习:

golangci-lint

我们将使用的第三个代码分析工具是 golangci-lint。和所有 Go 工具一样,可以通过多种方式安装它,包括使用 go install 命令:

1
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest

让我们验证安装是否成功,并检查版本:

1
2
3
golangci-lint --version
golangci-lint has version v1.61.0 built with go1.23.2
...

完美!一切正常。

遵循最小惊讶原则,golangci-lint 在没有参数时会运行所有 linter。

最小惊讶原则:在界面设计中,应始终做出最不令人惊讶的选择。

当我们检查之前克隆的 agent 仓库时会发生什么?golangci-lint 会显示相同的警告和建议吗?让我们找出答案。

和之前一样,从项目根目录开始扫描。

1
➜ golangci-lint run ./...

几乎立即,我们就注意到了一些改进代码的建议!例如:

1
2
3
src/extensions/nginx-app-protect/monitoring/processor/nap_test.go:60:14: S1025: the argument is already a string, there's no need to use fmt.Sprintf (gosimple)
 logEntry: fmt.Sprintf(`%s`, func() string {
 ^
1
2
3
src/plugins/common.go:85:5: S1009: should omit nil check; len() for []string is defined as zero (gosimple)
 if loadedConfig.Extensions != nil && len(loadedConfig.Extensions) > 0 {
    ^

linter 指出了需要注意的确切文件和代码行。我们的任务是评估代码,进行更改,第二次运行 linter 并运行所有单元测试。如果测试通过,我们就可以提交更新后的代码。完成了!当然,我们还需要推送更新到远程仓库。

检测竞态条件

当多个 goroutine 并发访问同一资源时,程序和库中可能出现竞态条件。当至少有一个 goroutine 尝试写入(更改)该资源时,就会检测到这些竞态条件。例如,该资源可能是一个作为计数器的全局或包级变量。这种情况可能会导致程序出现微妙且难以诊断和检测的错误。

Go 原生支持检测这种条件。我们可以使用 Go 的 test 工具并加上 -race 参数来运行测试。此方法将运行竞态检测器并有助于识别并发程序中的问题。

1
go test -race

需要特别注意的是,检测器只能评估已执行的代码路径,并会忽略未执行的代码路径。因此,首先运行静态代码分析工具并确保项目中没有所谓的 dead code 是至关重要的。

当我们告诉 Go:“嘿,使用 -race 参数运行测试”时,Go 编译器会启用竞态检测器编译代码。然后测试运行,并在运行时检查可能的竞态条件。当检测到竞态时,工具会打印详细报告,显示哪些 goroutine 尝试访问哪些资源。

另一种增加检测并发问题机会的方法是并行运行测试。为此,我们需要在测试中显式添加 t.Parallel()

两个并行执行的测试:

1
2
3
func TestParseDiskSpace(t *testing.T) {
    t.Parallel()
    ...
1
2
3
func TestParseMemoryUsage(t *testing.T) {
    t.Parallel()
    ...

检测竞态条件和设计并发代码是一个广泛且令人兴奋的话题,我们将在未来深入讨论。

扫描源代码中的漏洞

govulncheck

有多种工具可以扫描代码库中列于 CVE 数据库中的已知漏洞。

我们默认使用的工具是 govulncheck,它可以确保开发和发布的代码是安全的。可以在开发者的本地机器上安装并在提交代码到远程 Git 仓库之前进行扫描。

此外,还可以将扫描步骤集成到 GitHub 或 GitLab 的 CI 管道中,以便在每次合并请求时自动调用扫描,确保项目中不会引入漏洞。

govulncheck 是由 Go 团队开发的专用扫描器,使用的是一个包含 Go 漏洞的专用数据库。让我们在本地安装 govulncheck 并尝试其基本功能。

要安装最新版本,请运行以下命令:

1
go install golang.org/x/vuln/cmd/govulncheck@latest

接下来检查安装是否成功:

1
2
3
4
5
6
govulncheck -version
Go: go1.23.2
Scanner: govulncheck@v1.1.3
DB: https://vuln.go.dev
DB updated: 2024-10-17 15:37:30 +0000 UTC
...

现在可以运行首次扫描。让我们克隆 habit Git 仓库,然后进入项目的根目录并运行该工具。

1
2
➜ govulncheck
No vulnerabilities found.

看起来不错!我们没有在源代码中发现漏洞。这就结束了吗?还没完全结束!我们最初构建 habit 二进制文件时,go.mod 文件中定义了 Go 1.18 版本,而当前版本是 v1.23.2。

让我们扫描 habit 二进制文件,而不是源代码。

1
➜ govulncheck -mode binary -show verbose habit

我们在二进制模式下运行 govulncheck,这意味着可以扫描任何我们有权限访问的 Go 二进制文件,而无需源代码!此外,我们使用的是详细模式,它会显示完整的报告。最后的参数是要扫描的二进制文件名称。

嗯!这个报告看起来不太一样!发生了什么?

 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
36
37
38
39
40
Scanning your binary for known vulnerabilities...

Fetching vulnerabilities from the database...

Checking the binary against the vulnerabilities...

=== Symbol Results ===

No vulnerabilities found.

=== Package Results ===

Vulnerability #1: GO-2023-2186
    Incorrect detection of reserved device names on Windows in path/filepath
  More info: https://pkg.go.dev/vuln/GO-2023-2186
  Standard library
    Found in: path/filepath@go1.20.5
    Fixed in: path/filepath@go1.20.11

=== Module Results ===

Vulnerability #1: GO-2024-3107
    Stack exhaustion in Parse in go/build/constraint
  More info: https://pkg.go.dev/vuln/GO-2024-3107
  Standard library
    Found in: stdlib@go1.20.5
    Fixed in: stdlib@go1.22.7
...

Vulnerability #18: GO-2023-1878
    Insufficient sanitisation of Host header in net/http
  More info: https://pkg.go.dev/vuln/GO-2023-1878
  Standard library
    Found in: stdlib@go1.20.5
    Fixed in: stdlib@go1.20.6

Your code is affected by 0 vulnerabilities.
This scan also found 1 vulnerability in packages you import and 18
vulnerabilities in modules you require, but your code doesn't appear to call
these vulnerabilities.

报告的第一部分包含最重要的信息:未发现漏洞

其余部分包含在 Go 标准库中发现的其他漏洞的信息。那么我们是否受到了影响?我们的程序是否不安全?

最终的扫描报告告诉我们不必担心。程序似乎并未调用这些漏洞!太好了!

1
2
3
4
Your code is affected by 0 vulnerabilities.
This scan also found 1 vulnerability in packages you import and 18
vulnerabilities in modules you require, but your code doesn't appear to call
these vulnerabilities.

现在,让我们更新 go.mod 文件,将 Go 版本更改为最新的 1.23。接下来,运行 go mod tidy 以更新所有依赖项。此时,我们准备再次构建二进制文件。

1
➜ go build -o habit cmd/main.go

让我们重新运行扫描。

1
2
3
4
5
6
7
8
➜ govulncheck -mode binary -show verbose habit
Scanning your binary for known vulnerabilities...

Fetching vulnerabilities from the database...

Checking the binary against the vulnerabilities...

No vulnerabilities found.

这就是我们想要的结果!升级了 Go 版本,拉取了依赖项,并验证了软件和依赖项中没有 CVE 漏洞。

gosec

gosec 是一个静态代码分析工具,它可以帮助我们发现不安全的代码构造。可以在本地安装它,或者在 CI 管道中将其作为 GitHub Action 运行。正如之前所述,golangci-lint 包含了 gosec 作为插件,并在每次代码扫描时默认运行它。

让我们尝试一下并在本地安装扫描器。

1
go install github.com/securego/gosec/v2/cmd/gosec@latest

如果没有看到错误,gosec 就可以开始工作了。在运行首次扫描之前,我们先看看帮助说明:

1
2
3
4
5
6
7
gosec -h

gosec - Golang security checker

gosec analyses Go source code to look for common programming mistakes that
can lead to security problems.
...

我们可以使用一长串选项和规则来配置扫描器的行为。具体选项的详细信息超出了本文的范围。关于如何配置、运行和利用此 SAST 工具的详细教程即将推出!敬请期待!

为了尝试 gosec,我们需要克隆一个包含 Go 代码的 GitHub 仓库。

让我们克隆 brutus 仓库,这是一个开源的实验性 OSINT 应用程序,用于测试 Web 服务器配置。

1
git clone git@github.com:CyberRoute/bruter.git

接下来,将当前目录更改为项目的根目录并开始扫描。

1
➜ gosec ./...

几秒钟后,gosec 会显示扫描报告。我们可以立即学到什么?显示了一份按严重性和置信度排序的潜在问题列表。我们知道代码的哪些部分需要注意,以及问题对应的弱点分类。完美!接下来做什么?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
...

[/.../bruter/pkg/fuzzer/randomua.go:69] - G404 (CWE-338): Use of weak random number generator (math/rand or math/rand/v2 instead of crypto/rand) (Confidence: MEDIUM, Severity: HIGH)
    68:
  > 69:  randomIndex := rand.Intn(len(userAgents))
    70:  return userAgents[randomIndex]

...

[/.../bruter/pkg/server/config.go:40] - G402 (CWE-295): TLS InsecureSkipVerify set true. (Confidence: HIGH, Severity: HIGH)
    39:  customTransport := &http.Transport{
  > 40:   TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
    41:  }

...

在这个阶段,我们可以检查报告的 CWE,并了解所列出的漏洞的详细信息。例如,第二个问题将我们引导至 CWE-295 网站,在那里可以了解更多关于该漏洞的信息。

模糊测试

检查代码质量和发现漏洞的最后一种方法是模糊测试。模糊测试是一种特殊的自动化测试方法。它利用代码覆盖率来操纵随机生成的输入数据。

模糊测试对于发现潜在的安全缺陷(如缓冲区溢出、SQL 注入DoS 攻击XSS 攻击)非常有帮助。模糊测试的最重要特性是,它可以自动生成大量的输入组合!开发人员不需要冥思苦想来弄清楚数百甚至数千种输入数据组合!真是太棒了!

我们将在即将推出的教程中更详细地讨论模糊测试。

今天讨论的大多数方法和测试技术都得到了 OpenSSF 基金会的支持。希望获得最佳实践徽章的开源项目需要在许可、变更控制、漏洞报告、质量、安全性以及静态和动态安全代码分析等方面符合 FLOSS 标准。

保持安全,远离 CVE,并享受编程的乐趣!

正如 John Arundel 所说:

“编程很有趣,你应该享受其中的乐趣!”