使用 gosec 檢查 Go 代碼中的安全問題
Go 語言寫的代碼越來越常見,尤其是在容器、Kubernetes 或雲生態相關的開發中。Docker 是最早採用 Golang 的項目之一,隨後是 Kubernetes,之後大量的新項目在眾多編程語言中選擇了 Go。
像其他語言一樣,Go 也有它的長處和短處(如安全缺陷)。這些缺陷可能會因為語言本身的缺陷加上程序員編碼不當而產生,例如,C 代碼中的內存安全問題。
無論它們出現的原因是什麼,安全問題都應該在開發過程的早期修復,以免在封裝好的軟體中出現。幸運的是,靜態分析工具可以幫你以更可重複的方式處理這些問題。靜態分析工具通過解析用某種編程語言寫的代碼來找到問題。
這類工具中很多被稱為 linter。傳統意義上,linter 更注重的是檢查代碼中編碼問題、bug、代碼風格之類的問題,它們可能不會發現代碼中的安全問題。例如,Coverity 是一個很流行的工具,它可以幫助尋找 C/C++ 代碼中的問題。然而,也有一些工具專門用來檢查源碼中的安全問題。例如,Bandit 可以檢查 Python 代碼中的安全缺陷。而 gosec 則用來搜尋 Go 源碼中的安全缺陷。gosec
通過掃描 Go 的 AST( 抽象語法樹 )來檢查源碼中的安全問題。
開始使用 gosec
在開始學習和使用 gosec
之前,你需要準備一個 Go 語言寫的項目。有這麼多開源軟體,我相信這不是問題。你可以在 GitHub 的 熱門 Golang 倉庫中找一個。
本文中,我隨機選了 Docker CE 項目,但你可以選擇任意的 Go 項目。
安裝 Go 和 gosec
如果你還沒安裝 Go,你可以先從倉庫中拉取下來。如果你用的是 Fedora 或其他基於 RPM 的 Linux 發行版本:
$ dnf install golang.x86_64
如果你用的是其他操作系統,請參照 Golang 安裝頁面。
使用 version
參數來驗證 Go 是否安裝成功:
$ go version
go version go1.14.6 linux/amd64
運行 go get
命令就可以輕鬆地安裝 gosec
:
$ go get github.com/securego/gosec/cmd/gosec
上面這行命令會從 GitHub 下載 gosec
的源碼,編譯並安裝到指定位置。在倉庫的 README
中你還可以看到安裝該工具的其他方法。
gosec
的源碼會被下載到 $GOPATH
的位置,編譯出的二進位文件會被安裝到你系統上設置的 bin
目錄下。你可以運行下面的命令來查看 $GOPATH
和 $GOBIN
目錄:
$ go env | grep GOBIN
GOBIN="/root/go/gobin"
$ go env | grep GOPATH
GOPATH="/root/go"
如果 go get
命令執行成功,那麼 gosec
二進位應該就可以使用了:
$ ls -l ~/go/bin/
total 9260
-rwxr-xr-x. 1 root root 9482175 Aug 20 04:17 gosec
你可以把 $GOPATH
下的 bin
目錄添加到 $PATH
中。這樣你就可以像使用系統上的其他命令一樣來使用 gosec
命令行工具(CLI)了。
$ which gosec
/root/go/bin/gosec
$
使用 gosec
命令行工具的 -help
選項來看看運行是否符合預期:
$ gosec -help
gosec - Golang security checker
gosec analyzes Go source code to look for common programming mistakes that
can lead to security problems.
VERSION: dev
GIT TAG:
BUILD DATE:
USAGE:
之後,創建一個目錄,把源碼下載到這個目錄作為實例項目(本例中,我用的是 Docker CE):
$ mkdir gosec-demo
$ cd gosec-demo/
$ pwd
/root/gosec-demo
$ git clone https://github.com/docker/docker-ce.git
Cloning into 'docker-ce'...
remote: Enumerating objects: 1271, done.
remote: Counting objects: 100% (1271/1271), done.
remote: Compressing objects: 100% (722/722), done.
remote: Total 431003 (delta 384), reused 981 (delta 318), pack-reused 429732
Receiving objects: 100% (431003/431003), 166.84 MiB | 28.94 MiB/s, done.
Resolving deltas: 100% (221338/221338), done.
Updating files: 100% (10861/10861), done.
代碼統計工具(本例中用的是 cloc
)顯示這個項目大部分是用 Go 寫的,恰好迎合了 gosec
的功能。
$ ./cloc /root/gosec-demo/docker-ce/
10771 text files.
8724 unique files.
2560 files ignored.
--------------------------------------------------------------------------------Language files blank comment code
--------------------------------------------------------------------------------Go 7222 190785 230478 1574580
YAML 37 4831 817 156762
Markdown 529 21422 0 67893
Protocol Buffers 149 5014 16562 10071
使用默認選項運行 gosec
在 Docker CE 項目中使用默認選項運行 gosec
,執行 gosec ./...
命令。屏幕上會有很多輸出內容。在末尾你會看到一個簡短的 「Summary」,列出了瀏覽的文件數、所有文件的總行數,以及源碼中發現的問題數。
$ pwd
/root/gosec-demo/docker-ce
$ time gosec ./...
[gosec] 2020/08/20 04:44:15 Including rules: default
[gosec] 2020/08/20 04:44:15 Excluding rules: default
[gosec] 2020/08/20 04:44:15 Import directory: /root/gosec-demo/docker-ce/components/engine/opts
[gosec] 2020/08/20 04:44:17 Checking package: opts
[gosec] 2020/08/20 04:44:17 Checking file: /root/gosec-demo/docker-ce/components/engine/opts/address_pools.go
[gosec] 2020/08/20 04:44:17 Checking file: /root/gosec-demo/docker-ce/components/engine/opts/env.go
[gosec] 2020/08/20 04:44:17 Checking file: /root/gosec-demo/docker-ce/components/engine/opts/hosts.go
# End of gosec run
Summary:
Files: 1278
Lines: 173979
Nosec: 4
Issues: 644
real 0m52.019s
user 0m37.284s
sys 0m12.734s
$
滾動屏幕你會看到不同顏色高亮的行:紅色表示需要儘快查看的高優先順序問題,黃色表示中優先順序的問題。
關於誤判
在開始檢查代碼之前,我想先分享幾條基本原則。默認情況下,靜態檢查工具會基於一系列的規則對測試代碼進行分析,並報告出它們發現的所有問題。這是否意味著工具報出來的每一個問題都需要修復?非也。這個問題最好的解答者是設計和開發這個軟體的人。他們最熟悉代碼,更重要的是,他們了解軟體會在什麼環境下部署以及會被怎樣使用。
這個知識點對於判定工具標記出來的某段代碼到底是不是安全缺陷至關重要。隨著工作時間和經驗的積累,你會慢慢學會怎樣讓靜態分析工具忽略非安全缺陷,使報告內容的可執行性更高。因此,要判定 gosec
報出來的某個問題是否需要修復,讓一名有經驗的開發者對源碼做人工審計會是比較好的辦法。
高優先順序問題
從輸出內容看,gosec
發現了 Docker CE 的一個高優先順序問題,它使用的是低版本的 TLS( 傳輸層安全 )。無論什麼時候,使用軟體和庫的最新版本都是確保它更新及時、沒有安全問題的最好的方法。
[/root/gosec-demo/docker-ce/components/engine/daemon/logger/splunk/splunk.go:173] - G402 (CWE-295): TLS MinVersion too low. (Confidence: HIGH, Severity: HIGH)
172:
> 173: tlsConfig := &tls.Config{}
174:
它還發現了一個弱隨機數生成器。它是不是一個安全缺陷,取決於生成的隨機數的使用方式。
[/root/gosec-demo/docker-ce/components/engine/pkg/namesgenerator/names-generator.go:843] - G404 (CWE-338): Use of weak random number generator (math/rand instead of crypto/rand) (Confidence: MEDIUM, Severity: HIGH)
842: begin:
> 843: name := fmt.Sprintf("%s_%s", left[rand.Intn(len(left))], right[rand.Intn(len(right))])
844: if name == "boring_wozniak" /* Steve Wozniak is not boring */ {
中優先順序問題
這個工具還發現了一些中優先順序問題。它標記了一個通過與 tar
相關的解壓炸彈這種方式實現的潛在的 DoS 威脅,這種方式可能會被惡意的攻擊者利用。
[/root/gosec-demo/docker-ce/components/engine/pkg/archive/copy.go:357] - G110 (CWE-409): Potential DoS vulnerability via decompression bomb (Confidence: MEDIUM, Severity: MEDIUM)
356:
> 357: if _, err = io.Copy(rebasedTar, srcTar); err != nil {
358: w.CloseWithError(err)
它還發現了一個通過變數訪問文件的問題。如果惡意使用者能訪問這個變數,那麼他們就可以改變變數的值去讀其他文件。
[/root/gosec-demo/docker-ce/components/cli/cli/context/tlsdata.go:80] - G304 (CWE-22): Potential file inclusion via variable (Confidence: HIGH, Severity: MEDIUM)
79: if caPath != "" {
> 80: if ca, err = ioutil.ReadFile(caPath); err != nil {
81: return nil, err
文件和目錄通常是操作系統安全的最基礎的元素。這裡,gosec
報出了一個可能需要你檢查目錄的許可權是否安全的問題。
[/root/gosec-demo/docker-ce/components/engine/contrib/apparmor/main.go:41] - G301 (CWE-276): Expect directory permissions to be 0750 or less (Confidence: HIGH, Severity: MEDIUM)
40: // make sure /etc/apparmor.d exists
> 41: if err := os.MkdirAll(path.Dir(apparmorProfilePath), 0755); err != nil {
42: log.Fatal(err)
你經常需要在源碼中啟動命令行工具。Go 使用內建的 exec 庫來實現。仔細地分析用來調用這些工具的變數,就能發現安全缺陷。
[/root/gosec-demo/docker-ce/components/engine/testutil/fakestorage/fixtures.go:59] - G204 (CWE-78): Subprocess launched with variable (Confidence: HIGH, Severity: MEDIUM)
58:
> 59: cmd := exec.Command(goCmd, "build", "-o", filepath.Join(tmp, "httpserver"), "github.com/docker/docker/contrib/httpserver")
60: cmd.Env = append(os.Environ(), []string{
低優先順序問題
在這個輸出中,gosec 報出了一個 unsafe
調用相關的低優先順序問題,這個調用會繞開 Go 提供的內存保護。再仔細分析下你調用 unsafe
的方式,看看是否有被別人利用的可能性。
[/root/gosec-demo/docker-ce/components/engine/pkg/archive/changes_linux.go:264] - G103 (CWE-242): Use of unsafe calls should be audited (Confidence: HIGH, Severity: LOW)
263: for len(buf) > 0 {
> 264: dirent := (*unix.Dirent)(unsafe.Pointer(&buf[0]))
265: buf = buf[dirent.Reclen:]
[/root/gosec-demo/docker-ce/components/engine/pkg/devicemapper/devmapper_wrapper.go:88] - G103 (CWE-242): Use of unsafe calls should be audited (Confidence: HIGH, Severity: LOW)
87: func free(p *C.char) {
> 88: C.free(unsafe.Pointer(p))
89: }
它還標記了源碼中未處理的錯誤。源碼中出現的錯誤你都應該處理。
[/root/gosec-demo/docker-ce/components/cli/cli/command/image/build/context.go:172] - G104 (CWE-703): Errors unhandled. (Confidence: HIGH, Severity: LOW)
171: err := tar.Close()
> 172: os.RemoveAll(dockerfileDir)
173: return err
自定義 gosec 掃描
使用 gosec
的默認選項會帶來很多的問題。然而,經過人工審計,隨著時間推移你會掌握哪些問題是不需要標記的。你可以自己指定排除和包含哪些測試。
我上面提到過,gosec
是基於一系列的規則從 Go 源碼中查找問題的。下面是它使用的完整的規則列表:
- G101:查找硬編碼憑證
- G102:綁定到所有介面
- G103:審計
unsafe
塊的使用 - G104:審計未檢查的錯誤
- G106:審計
ssh.InsecureIgnoreHostKey
的使用 - G107: 提供給 HTTP 請求的 url 作為污點輸入
- G108:
/debug/pprof
上自動暴露的剖析端點 - G109:
strconv.Atoi
轉換到 int16 或 int32 時潛在的整數溢出 - G110: 潛在的通過解壓炸彈實現的 DoS
- G201:SQL 查詢構造使用格式字元串
- G202:SQL 查詢構造使用字元串連接
- G203:在 HTML 模板中使用未轉義的數據
- G204:審計命令執行情況
- G301:創建目錄時文件許可權分配不合理
- G302:使用
chmod
時文件許可權分配不合理 - G303:使用可預測的路徑創建臨時文件
- G304:通過污點輸入提供的文件路徑
- G305:提取 zip/tar 文檔時遍歷文件
- G306: 寫到新文件時文件許可權分配不合理
- G307: 把返回錯誤的函數放到
defer
內 - G401:檢測 DES、RC4、MD5 或 SHA1 的使用
- G402:查找錯誤的 TLS 連接設置
- G403:確保最小 RSA 密鑰長度為 2048 位
- G404:不安全的隨機數源(
rand
) - G501:導入黑名單列表:crypto/md5
- G502:導入黑名單列表:crypto/des
- G503:導入黑名單列表:crypto/rc4
- G504:導入黑名單列表:net/http/cgi
- G505:導入黑名單列表:crypto/sha1
- G601: 在
range
語句中使用隱式的元素別名
排除指定的測試
你可以自定義 gosec
來避免對已知為安全的問題進行掃描和報告。你可以使用 -exclude
選項和上面的規則編號來忽略指定的問題。
例如,如果你不想讓 gosec
檢查源碼中硬編碼憑證相關的未處理的錯誤,那麼你可以運行下面的命令來忽略這些錯誤:
$ gosec -exclude=G104 ./...
$ gosec -exclude=G104,G101 ./...
有時候你知道某段代碼是安全的,但是 gosec
還是會報出問題。然而,你又不想完全排除掉整個檢查,因為你想讓 gosec
檢查新增的代碼。通過在你已知為安全的代碼塊添加 #nosec
標記可以避免 gosec
掃描。這樣 gosec
會繼續掃描新增代碼,而忽略掉 #nosec
標記的代碼塊。
運行指定的檢查
另一方面,如果你只想檢查指定的問題,你可以通過 -include
選項和規則編號來告訴 gosec
運行哪些檢查:
$ gosec -include=G201,G202 ./...
掃描測試文件
Go 語言自帶對測試的支持,通過單元測試來檢驗一個元素是否符合預期。在默認模式下,gosec
會忽略測試文件,你可以使用 -tests
選項把它們包含進來:
gosec -tests ./...
修改輸出的格式
找出問題只是它的一半功能;另一半功能是把它檢查到的問題以用戶友好同時又方便工具處理的方式報告出來。幸運的是,gosec
可以用不同的方式輸出。例如,如果你想看 JSON 格式的報告,那麼就使用 -fmt
選項指定 JSON 格式並把結果保存到 results.json
文件中:
$ gosec -fmt=json -out=results.json ./...
$ ls -l results.json
-rw-r--r--. 1 root root 748098 Aug 20 05:06 results.json
$
{
"severity": "LOW",
"confidence": "HIGH",
"cwe": {
"ID": "242",
"URL": "https://cwe.mitre.org/data/definitions/242.html"
},
"rule_id": "G103",
"details": "Use of unsafe calls should be audited",
"file": "/root/gosec-demo/docker-ce/components/engine/daemon/graphdriver/graphtest/graphtest_unix.go",
"code": "304: t// Cast to []byten305: theader := *(*reflect.SliceHeader)(unsafe.Pointer(u0026buf))n306: theader. Len *= 8n",
"line": "305",
"column": "36"
},
用 gosec 檢查容易被發現的問題
靜態檢查工具不能完全代替人工代碼審計。然而,當代碼量變大、有眾多開發者時,這樣的工具往往有助於以可重複的方式找出容易被發現的問題。它對於幫助新開發者識別和在編碼時避免引入這些安全缺陷很有用。
via: https://opensource.com/article/20/9/gosec
作者:Gaurav Kamathe 選題:lujun9972 譯者:lxbowlf 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive