撰寫安全的 Go 程式碼
Go 版本升級,安全修補到位,相依無憂。
作者: Brian Morrison II
撰寫 Go 程式碼時,如何兼顧安全性?這篇文章將聚焦幾個重要的實務做法,幫助你持續寫出強健、安全又有效率的程式碼。
- 我們該如何掌握 Go 的安全公告?
- 如何讓 Go 程式碼保持更新並修補漏洞?
- 如何測試 Go 程式碼的安全性及強健性?
- 什麼是 CVE?哪裡可以學到常見的軟體漏洞資訊?
郵件清單
首先,最直接的方法就是訂閱 Go 郵件清單( [email protected]
)。所有包含安全性修正的版本都會公告於此,確保你不會錯過任何重要資訊。
保持 Go 版本更新
第二步,務必讓專案中的 Go 版本保持最新。即使你沒有使用最新的語言特性,更新版本也能獲得所有已知漏洞的安全性修補,並確保與新的相依套件相容,避免應用程式發生整合問題。
第三步,了解各 Go 版本中修復了哪些安全問題和 CVE。你可以參考 Go 版本歷史記錄網站,並更新專案 go.mod
檔案中的版本。
升級 Go 版本後,務必確認不會造成相容性和相依性問題,尤其是第三方套件。大型專案通常有許多套件相依,風險也更高。
重點在於將風險降到最低,避免套件、API 或函式簽章變更造成的程式碼重構。
使用 Go 工具
確認 Go 版本沒有安全疑慮後,就可以專注於程式碼本身。我們可以使用靜態程式碼分析器來評估程式碼品質和安全性。
vet
開始使用第三方分析器前,建議先使用 Go 內建的 go vet
指令。
go vet
指令會掃描原始碼,回報潛在問題,包括語法錯誤和可能導致執行錯誤的程式碼結構。
常見問題包括 goroutine 錯誤、未使用的變數和無法到達的程式碼區塊。 go vet
的主要優點是它已包含在 Go 工具箱中。
我們會在其他文章深入探討 vet
的細節。 go vet
的 網站 有豐富的文件和範例。
staticcheck
Staticcheck 是另一個靜態程式碼分析器,可以找出程式碼錯誤、潛在效能問題,並檢查 Go 語言風格。它會簡化程式碼、說明問題並提供修正建議及範例。
除了在 CI 流程中使用 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 GitHub 儲存庫中可以找到什麼。首先,複製儲存庫:
git clone [email protected]: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 網站提供更多關於這些弱點的資訊,供日後參考:
golangci-lint
第三個程式碼分析器是 golangci-lint
。它可以透過多種方式安裝,包括 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 嘗試寫入資源時,例如修改全域變數或套件層級的計數器,就更容易發生難以診斷的錯誤。
Go 內建支援偵測競爭條件。使用 go test -race
指令即可執行競爭偵測器,找出併發程式中的問題。
go test -race
需要注意的是,偵測器只會檢查執行的程式碼,不會檢查未執行的程式碼路徑。因此,務必先執行靜態程式碼分析,移除專案中的無用程式碼。
-race
參數會讓 Go 編譯器在編譯時啟用競爭偵測器,並在測試執行時檢查可能的競爭條件。如果偵測到競爭,會顯示詳細報告,說明哪些 goroutine 嘗試存取哪些資源。
另一個提高偵測機率的方法是平行執行測試。在測試中加入 t.Parallel()
即可。
兩個平行執行的測試:
func TestParseDiskSpace(t *testing.T) {
t.Parallel()
...
func TestParseMemoryUsage(t *testing.T) {
t.Parallel()
...
偵測競爭條件和設計併發程式碼是另一個廣泛且重要的主題,我們會在之後的文章中討論。
掃描原始碼中的漏洞
govulncheck
有許多工具可以掃描程式碼,找出 CVE 資料庫中列出的已知漏洞。
govulncheck
是 Go 團隊開發的工具,可以協助我們開發及發布安全的程式碼。它可以在開發者的電腦上執行,也可以整合到 GitHub 或 GitLab 的 CI 流程中,在每次合併請求時執行掃描,避免引入新的漏洞。
govulncheck
使用一個專門的 Go 漏洞資料庫。讓我們安裝 govulncheck
並試用它的基本功能。
安裝最新版本:
go install golang.org/x/vuln/cmd/govulncheck@latest
檢查安裝是否成功:
govulncheck -version
Go: go1.23.2
Scanner: [email protected]
DB: https://vuln.go.dev
DB updated: 2024-10-17 15:37:30 +0000 UTC
...
我們來執行第一次掃描。複製 habit git 儲存庫,並在根目錄執行工具:
➜ habit git:(main) ✗ govulncheck
No vulnerabilities found.
看起來不錯!沒有發現漏洞。這樣就結束了嗎?還沒!建置 habit 執行檔時, go.mod
檔案指定的 Go 版本是 1.18,而目前的版本是 1.23.2。
我們來掃描 habit 執行檔,而不是原始碼:
➜ habit git:(main) ✗ govulncheck -mode binary -show verbose habit
這裡使用 -mode binary
參數指定掃描執行檔,這表示我們不需要原始碼也能掃描! -show verbose
參數會顯示完整的報告,包含多個區塊。最後一個參數是執行檔的名稱。
嗯!這次的報告看起來不一樣!發生什麼事了?
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/[email protected]
Fixed in: path/[email protected]
=== 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: [email protected]
Fixed in: [email protected]
...
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: [email protected]
Fixed in: [email protected]
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.
第一個區塊顯示最重要的訊息: No vulnerabilities found。
其他區塊顯示在 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.
Perfect!我們升級了 Go 版本、更新了相依套件,並確認程式碼和相依套件都沒有 CVE 漏洞。
gosec
gosec
是一個靜態程式碼分析器,可以找出不安全的程式碼。它可以安裝在本地,也可以作為 GitHub Action 在 CI 流程中執行。前面提到的 golangci-lint
也包含 gosec
外掛,並在每次掃描時預設執行。
我們來安裝 gosec
試試看:
go install github.com/securego/gosec/v2/cmd/gosec@latest
如果沒有錯誤訊息,就可以開始使用了。執行第一次掃描前,先看看它的說明:
gosec -h
gosec - Golang security checker
gosec analyses Go source code to look for common programming mistakes that
can lead to security problems.
...
它有許多選項和規則可以設定。詳細的設定教學將在後續文章中說明。
我們需要複製一個 GitHub 儲存庫來測試 gosec
。
複製 brutus 儲存庫,這是一個開源的實驗性 OSINT 應用程式,用於測試網頁伺服器設定。
git clone [email protected]:CyberRoute/bruter.git
接著,在專案根目錄執行掃描:
gosec ./...
幾秒鐘後, gosec
就會顯示掃描報告。我們可以立刻看到依嚴重程度和可信度排序的潛在問題清單,以及相關的弱點分類。接下來,我們可以參考 CWE 網站,例如 CWE-295,了解更多關於弱點的資訊。
...
[/.../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: }
...
模糊測試
最後一個檢查程式碼品質和找出漏洞的方法是模糊測試(Fuzzing)。這是一種自動化測試,它會使用程式碼測試覆蓋率來產生隨機輸入資料,並嘗試找出程式錯誤,例如緩衝區溢位、 SQL 注入、 DoS 攻擊 和 XSS 攻擊。模糊測試最大的優點是它可以自動產生大量的輸入組合,讓開發者不必費心思考各種可能的輸入。
我們會在後續文章中更詳細地介紹模糊測試。
OpenSSF 基金會鼓勵使用上述這些方法和測試技術。想要獲得最佳實踐徽章的開源專案必須符合 FLOSS 標準,包括授權、變更控制、漏洞回報、程式碼品質、安全性以及靜態和動態程式碼分析等。
保持程式碼安全、遠離 CVE,享受程式設計的樂趣!
就像 John Arundel 說的:
「程式設計很有趣,你應該樂在其中!」
下次見!