编写安全的 Go 代码

文摘   2024-11-06 11:21   美国  

在编写Go代码时如何保持安全意识?在一篇短文中回答这个问题似乎不太可能。因此,我们将重点关注几个具体的实践。

持续应用这些实践将有助于编写健壮、安全和高性能的代码。

  • 我们如何及时了解Go安全公告?
  • 我们如何保持Go代码打补丁和更新?
  • 我们如何从安全性和健壮性的角度测试Go代码?
  • 什么是CVE,我们在哪里可以了解最常见的软件漏洞?

邮件列表

让我们从最明显的地方开始 - Go邮件列表。我们需要订阅以便从源头获取所有关键的安全信息。所有包含安全修复的版本都会在golang-announce@googlegroups.com列表中公告。一旦我们订阅了该列表,就可以确保不会错过任何重要公告。

保持Go版本更新

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

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

升级到新版本的Go后,我们应该确保这个操作不会引入兼容性和依赖性问题,特别是对于第三方包。当我们在有数十甚至上百个直接和间接包依赖的大型项目中工作时,这可能会更有风险。

关键是通过消除潜在的依赖性问题来控制风险。这些问题可能包括需要紧急重构现有代码以使其与新依赖项一起工作。此类问题的例子包括更改的包、API或函数签名。

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

vet

在安装和使用第三方分析器之前,最好使用Go"原生"的go vet命令。

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

最常见的问题包括goroutine错误、未使用的变量和代码库中无法访问的区域。使用go vet命令的主要优势是它是Go工具箱的一部分。

在另一篇文章中,我们将深入研究vet的细节。在go vet  网站[2] 上有详细的文档和示例。

staticcheck

Staticcheck是另一个静态代码分析器。它是一个第三方linter,可以帮助发现bug并检测可能的性能问题。它还强制执行Go语言的样式。它提供代码简化建议,解释发现的问题并提供带有示例的修正建议。

除了在CI管道中运行staticcheck外,我们还可以在笔记本电脑上安装staticcheck作为独立二进制文件,并在本地扫描代码。让我们安装最新版本:

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

终端上没有错误?如果是这样,我们就可以开始扫描了。但首先,让我们检查已安装的版本以确保一切正常。

staticcheck --version
staticcheck 2024.1.1 (0.5.1)

go vet类似,不带参数运行staticcheck默认会调用所有代码分析器。这种方法很好地遵循了UNIX编程哲学,即使用合理的默认值而不强迫用户做不必要的工作。

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

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

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

➜  agent git:(main) ✗ staticcheck ./...

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

  • 已弃用的包、方法或函数,例如:

...
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)

  • 未使用的变量和字段,例如:

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)

  • 与代码质量相关的可能问题,例如:

src/core/nginx.go:791:4: ineffective break statement. Did you mean to break out of the outer loop? (SA4011)

现在,我们可以开始分析突出显示的问题。在这篇介绍性文章中,详细深入研究代码库超出了范围。我们将在即将发布的文章中进行更深入的代码分析,展示示例,并修复安全和性能问题。

现在,让我们记下包含大量关于列出的弱点信息的CWE网站,以便我们以后可以研究它们:

  • 未使用的变量  CWE-563[4]
  • 使用已弃用的结构  CWE-477[5]

golangci-lint

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

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

让我们验证安装是否顺利并检查版本:

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

完美!一切看起来都很好。

遵循最小惊讶原则,当我们不带参数调用golangci-lint时,它会运行所有linter。

最小惊讶原则:在接口设计中,始终做最不令人惊讶的事情。

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

像之前一样,我们将从项目的根目录开始扫描。

➜  agent git:(main) ✗ golangci-lint run ./...

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

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 {
 ^

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尝试写入(更改)资源时,就会检测到这些条件。例如,资源可以是充当计数器的全局包级变量。程序中的这种情况可能导致微妙的、非常难以诊断和检测的bug。

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

我们需要记住一个警告。检测器只能评估已执行的代码,并将忽略未执行的代码路径。因此,首先运行静态代码分析器并确保我们的项目中没有所谓的死代码是至关重要的。

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

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

并行执行的两个测试

func TestParseDiskSpace(t *testing.T) {
    t.Parallel()
    ...

func TestParseMemoryUsage(t *testing.T) {
    t.Parallel()
    ...

检测竞态条件和设计并发代码是一个广泛而有趣的主题,我们将在未来讨论。

扫描源代码中的漏洞

govulncheck

我们有很多工具可以扫描代码库中列在 CVEs[6] 数据库中的已知漏洞。

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

或者,我们可以将扫描步骤与GitHub或GitLab中的CI管道集成。然后,可以在每个合并请求时调用扫描,以确保我们不会在项目中引入漏洞。

govulncheck由Go团队开发。专用的Go漏洞数据库为扫描器提供信息。让我们在本地安装govulncheck并尝试基本功能。

要安装最新版本,我们需要运行以下命令:

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

是时候检查安装过程是否顺利了:

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[7] git仓库。然后,导航到其根目录并运行该工具。

➜  habit git:(main) ✗ govulncheck
No vulnerabilities found.

看起来很有希望!我们在源代码中没有发现漏洞。我们完成了吗?不是真的!我们在go.mod文件定义Go 1.18版本时构建了habit二进制文件。当前版本是v1.23.2。

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

  habit git:(main)  govulncheck -mode binary -show verbose habit

我们在二进制模式下运行govulncheck。这意味着我们可以扫描我们有权访问的任何Go二进制文件!我们不需要源代码!然后,我们以详细模式运行扫描。它将显示分成多个部分的完整报告。最后一个参数是我们要扫描的二进制文件的名称。

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

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库中发现的其他漏洞的信息。好的,但我们受影响吗?我们的程序不安全吗?

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

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以使所有依赖项保持最新。此时,我们准备再次构建二进制文件。

  habit git:(main)  go build -o habit cmd/main.go

让我们重新运行扫描。

  habit git:(main)  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[8] 是一个静态代码分析器。它有助于发现不安全的代码结构。我们可以在笔记本电脑上本地安装它,或者在CI管道中作为GitHub Action运行它。如前所述,golangci-lint包含gosec作为插件,并在每次代码扫描时默认运行它。

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

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

如果我们没有看到错误,gosec就准备好了。在运行第一次扫描之前,让我们看看菜单:

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[9] 仓库。这是一个用于测试Web服务器配置的开源实验性 OSINT[10] 应用程序。

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

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

几秒钟后,gosec呈现扫描报告。我们立即可以了解到什么?我们看到按严重性和置信度排序的潜在问题列表。我们知道代码的哪些部分需要注意,以及问题适用于哪种弱点分类。完美!接下来呢?

...

[/.../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[11] 网站,在那里我们可以了解更多关于漏洞的信息。

模糊测试

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

它在发现潜在的安全漏洞方面非常有帮助,如缓冲区溢出、 SQL注入[12] 、 DoS攻击[13] 和 XSS攻击[14] 。模糊测试最重要的特点是许多输入组合是自动生成的!开发人员不需要绞尽脑汁试图找出数百甚至数千个输入数据组合!多么令人欣慰!

我们将在即将发布的教程中更详细地关注模糊测试。

我们今天讨论的大多数方法和测试技术都得到了 OpenSSF[15] 基金会的鼓励。想要获得最佳实践徽章的开源项目需要在许可、变更控制、漏洞报告、质量、安全以及静态和动态安全代码分析等领域满足 FLOSS标准[16]

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

正如  John Arundel[17]  所说:

"编程是一件有趣的事,你也应该从中获得乐趣!"

下次再见!

参考链接

  1. Go: https://jarosz.dev/categories/go/
  2. 网站: https://golang.google.cn/cmd/vet/
  3. NGINX Agent: https://github.com/nginx/agent
  4. CWE-563: https://cwe.mitre.org/data/definitions/563.html
  5. CWE-477: https://cwe.mitre.org/data/definitions/477.html
  6. CVEs: https://www.cve.org/
  7. habit: https://github.com/qba73/habit
  8. gosec: https://github.com/securego/gosec
  9. brutus: https://github.com/CyberRoute/bruter
  10. OSINT: https://en.wikipedia.org/wiki/Open-source_intelligence
  11. CWE-295: https://cwe.mitre.org/data/definitions/295.html
  12. SQL注入: https://owasp.org/www-community/attacks/SQL_Injection
  13. DoS攻击: https://owasp.org/www-community/attacks/Denial_of_Service
  14. XSS攻击: https://owasp.org/www-community/attacks/xss/
  15. OpenSSF: https://openssf.org/best-practices-badge/
  16. FLOSS标准: https://www.bestpractices.dev/en/criteria/0
  17. John Arundel: https://bitfieldconsulting.com/

幻想发生器
图解技术本质
 最新文章