使用 gosec 檢查 Go 代碼中的安全問(wèn)題
Go 語(yǔ)言 寫(xiě)的代碼越來(lái)越常見(jiàn),尤其是在容器、Kubernetes 或云生態(tài)相關(guān)的開(kāi)發(fā)中。Docker 是最早采用 Golang 的項(xiàng)目之一,隨后是 Kubernetes,之后大量的新項(xiàng)目在眾多編程語(yǔ)言中選擇了 Go。
像其他語(yǔ)言一樣,Go 也有它的長(zhǎng)處和短處(如安全缺陷)。這些缺陷可能會(huì)因?yàn)檎Z(yǔ)言本身的缺陷加上程序員編碼不當(dāng)而產(chǎn)生,例如,C 代碼中的內(nèi)存安全問(wèn)題。
無(wú)論它們出現(xiàn)的原因是什么,安全問(wèn)題都應(yīng)該在開(kāi)發(fā)過(guò)程的早期修復(fù),以免在封裝好的軟件中出現(xiàn)。幸運(yùn)的是,靜態(tài)分析工具可以幫你以更可重復(fù)的方式處理這些問(wèn)題。靜態(tài)分析工具通過(guò)解析用某種編程語(yǔ)言寫(xiě)的代碼來(lái)找到問(wèn)題。
這類工具中很多被稱為 linter。傳統(tǒng)意義上,linter 更注重的是檢查代碼中編碼問(wèn)題、bug、代碼風(fēng)格之類的問(wèn)題,它們可能不會(huì)發(fā)現(xiàn)代碼中的安全問(wèn)題。例如, Coverity 是一個(gè)很流行的工具,它可以幫助尋找 C/C++ 代碼中的問(wèn)題。然而,也有一些工具專門用來(lái)檢查源碼中的安全問(wèn)題。例如, Bandit 可以檢查 Python 代碼中的安全缺陷。而 gosec 則用來(lái)搜尋 Go 源碼中的安全缺陷。gosec 通過(guò)掃描 Go 的 AST( 抽象語(yǔ)法樹(shù)(abstract syntax tree))來(lái)檢查源碼中的安全問(wèn)題。
開(kāi)始使用 gosec
在開(kāi)始學(xué)習(xí)和使用 gosec 之前,你需要準(zhǔn)備一個(gè) Go 語(yǔ)言寫(xiě)的項(xiàng)目。有這么多開(kāi)源軟件,我相信這不是問(wèn)題。你可以在 GitHub 的 熱門 Golang 倉(cāng)庫(kù) 中找一個(gè)。
本文中,我隨機(jī)選了 Docker CE 項(xiàng)目,但你可以選擇任意的 Go 項(xiàng)目。
安裝 Go 和 gosec
如果你還沒(méi)安裝 Go,你可以先從倉(cāng)庫(kù)中拉取下來(lái)。如果你用的是 Fedora 或其他基于 RPM 的 Linux 發(fā)行版本:
- $ dnf install golang.x86_64
如果你用的是其他操作系統(tǒng),請(qǐng)參照 Golang 安裝 頁(yè)面。
使用 version 參數(shù)來(lái)驗(yàn)證 Go 是否安裝成功:
- $ go version
- go version go1.14.6 linux/amd64
運(yùn)行 go get 命令就可以輕松地安裝 gosec:
- $ go get github.com/securego/gosec/cmd/gosec
上面這行命令會(huì)從 GitHub 下載 gosec 的源碼,編譯并安裝到指定位置。在倉(cāng)庫(kù)的 README 中你還可以看到 安裝該工具的其他方法 。
gosec 的源碼會(huì)被下載到 $GOPATH 的位置,編譯出的二進(jìn)制文件會(huì)被安裝到你系統(tǒng)上設(shè)置的 bin 目錄下。你可以運(yùn)行下面的命令來(lái)查看 $GOPATH 和 $GOBIN 目錄:
- $ go env | grep GOBIN
- GOBIN="/root/go/gobin"
- $ go env | grep GOPATH
- GOPATH="/root/go"
如果 go get 命令執(zhí)行成功,那么 gosec 二進(jìn)制應(yīng)該就可以使用了:
- $ ls -l ~/go/bin/
- total 9260
- -rwxr-xr-x. 1 root root 9482175 Aug 20 04:17 gosec
你可以把 $GOPATH 下的 bin 目錄添加到 $PATH 中。這樣你就可以像使用系統(tǒng)上的其他命令一樣來(lái)使用 gosec 命令行工具(CLI)了。
- $ which gosec
- /root/go/bin/gosec
- $
使用 gosec 命令行工具的 -help 選項(xiàng)來(lái)看看運(yùn)行是否符合預(yù)期:
- $ 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:
之后,創(chuàng)建一個(gè)目錄,把源碼下載到這個(gè)目錄作為實(shí)例項(xiàng)目(本例中,我用的是 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.
代碼統(tǒng)計(jì)工具(本例中用的是 cloc)顯示這個(gè)項(xiàng)目大部分是用 Go 寫(xiě)的,恰好迎合了 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
使用默認(rèn)選項(xiàng)運(yùn)行 gosec
在 Docker CE 項(xiàng)目中使用默認(rèn)選項(xiàng)運(yùn)行 gosec,執(zhí)行 gosec ./... 命令。屏幕上會(huì)有很多輸出內(nèi)容。在末尾你會(huì)看到一個(gè)簡(jiǎn)短的 “Summary”,列出了瀏覽的文件數(shù)、所有文件的總行數(shù),以及源碼中發(fā)現(xiàn)的問(wèn)題數(shù)。
- $ 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
- $
滾動(dòng)屏幕你會(huì)看到不同顏色高亮的行:紅色表示需要盡快查看的高優(yōu)先級(jí)問(wèn)題,黃色表示中優(yōu)先級(jí)的問(wèn)題。
關(guān)于誤判
在開(kāi)始檢查代碼之前,我想先分享幾條基本原則。默認(rèn)情況下,靜態(tài)檢查工具會(huì)基于一系列的規(guī)則對(duì)測(cè)試代碼進(jìn)行分析,并報(bào)告出它們發(fā)現(xiàn)的所有問(wèn)題。這是否意味著工具報(bào)出來(lái)的每一個(gè)問(wèn)題都需要修復(fù)?非也。這個(gè)問(wèn)題最好的解答者是設(shè)計(jì)和開(kāi)發(fā)這個(gè)軟件的人。他們最熟悉代碼,更重要的是,他們了解軟件會(huì)在什么環(huán)境下部署以及會(huì)被怎樣使用。
這個(gè)知識(shí)點(diǎn)對(duì)于判定工具標(biāo)記出來(lái)的某段代碼到底是不是安全缺陷至關(guān)重要。隨著工作時(shí)間和經(jīng)驗(yàn)的積累,你會(huì)慢慢學(xué)會(huì)怎樣讓靜態(tài)分析工具忽略非安全缺陷,使報(bào)告內(nèi)容的可執(zhí)行性更高。因此,要判定 gosec 報(bào)出來(lái)的某個(gè)問(wèn)題是否需要修復(fù),讓一名有經(jīng)驗(yàn)的開(kāi)發(fā)者對(duì)源碼做人工審計(jì)會(huì)是比較好的辦法。
高優(yōu)先級(jí)問(wèn)題
從輸出內(nèi)容看,gosec 發(fā)現(xiàn)了 Docker CE 的一個(gè)高優(yōu)先級(jí)問(wèn)題,它使用的是低版本的 TLS( 傳輸層安全(Transport Layer Security)())。無(wú)論什么時(shí)候,使用軟件和庫(kù)的最新版本都是確保它更新及時(shí)、沒(méi)有安全問(wèn)題的最好的方法。
- [/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:
它還發(fā)現(xiàn)了一個(gè)弱隨機(jī)數(shù)生成器。它是不是一個(gè)安全缺陷,取決于生成的隨機(jī)數(shù)的使用方式。
- [/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 */ {
中優(yōu)先級(jí)問(wèn)題
這個(gè)工具還發(fā)現(xiàn)了一些中優(yōu)先級(jí)問(wèn)題。它標(biāo)記了一個(gè)通過(guò)與 tar 相關(guān)的解壓炸彈這種方式實(shí)現(xiàn)的潛在的 DoS 威脅,這種方式可能會(huì)被惡意的攻擊者利用。
- [/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)
它還發(fā)現(xiàn)了一個(gè)通過(guò)變量訪問(wèn)文件的問(wèn)題。如果惡意使用者能訪問(wèn)這個(gè)變量,那么他們就可以改變變量的值去讀其他文件。
- [/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
文件和目錄通常是操作系統(tǒng)安全的最基礎(chǔ)的元素。這里,gosec 報(bào)出了一個(gè)可能需要你檢查目錄的權(quán)限是否安全的問(wèn)題。
- [/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)
你經(jīng)常需要在源碼中啟動(dòng)命令行工具。Go 使用內(nèi)建的 exec 庫(kù)來(lái)實(shí)現(xiàn)。仔細(xì)地分析用來(lái)調(diào)用這些工具的變量,就能發(fā)現(xiàn)安全缺陷。
- [/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{
低優(yōu)先級(jí)問(wèn)題
在這個(gè)輸出中,gosec 報(bào)出了一個(gè) unsafe 調(diào)用相關(guān)的低優(yōu)先級(jí)問(wèn)題,這個(gè)調(diào)用會(huì)繞開(kāi) Go 提供的內(nèi)存保護(hù)。再仔細(xì)分析下你調(diào)用 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: bufbuf = 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: }
它還標(biāo)記了源碼中未處理的錯(cuò)誤。源碼中出現(xiàn)的錯(cuò)誤你都應(yīng)該處理。
- [/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 的默認(rèn)選項(xiàng)會(huì)帶來(lái)很多的問(wèn)題。然而,經(jīng)過(guò)人工審計(jì),隨著時(shí)間推移你會(huì)掌握哪些問(wèn)題是不需要標(biāo)記的。你可以自己指定排除和包含哪些測(cè)試。
我上面提到過(guò),gosec 是基于一系列的規(guī)則從 Go 源碼中查找問(wèn)題的。下面是它使用的完整的 規(guī)則 列表:
- G101:查找硬編碼憑證
- G102:綁定到所有接口
- G103:審計(jì) unsafe 塊的使用
- G104:審計(jì)未檢查的錯(cuò)誤
- G106:審計(jì) ssh.InsecureIgnoreHostKey 的使用
- G107: 提供給 HTTP 請(qǐng)求的 url 作為污點(diǎn)輸入
- G108: /debug/pprof 上自動(dòng)暴露的剖析端點(diǎn)
- G109: strconv.Atoi 轉(zhuǎn)換到 int16 或 int32 時(shí)潛在的整數(shù)溢出
- G110: 潛在的通過(guò)解壓炸彈實(shí)現(xiàn)的 DoS
- G201:SQL 查詢構(gòu)造使用格式字符串
- G202:SQL 查詢構(gòu)造使用字符串連接
- G203:在 HTML 模板中使用未轉(zhuǎn)義的數(shù)據(jù)
- G204:審計(jì)命令執(zhí)行情況
- G301:創(chuàng)建目錄時(shí)文件權(quán)限分配不合理
- G302:使用 chmod 時(shí)文件權(quán)限分配不合理
- G303:使用可預(yù)測(cè)的路徑創(chuàng)建臨時(shí)文件
- G304:通過(guò)污點(diǎn)輸入提供的文件路徑
- G305:提取 zip/tar 文檔時(shí)遍歷文件
- G306: 寫(xiě)到新文件時(shí)文件權(quán)限分配不合理
- G307: 把返回錯(cuò)誤的函數(shù)放到 defer 內(nèi)
- G401:檢測(cè) DES、RC4、MD5 或 SHA1 的使用
- G402:查找錯(cuò)誤的 TLS 連接設(shè)置
- G403:確保最小 RSA 密鑰長(zhǎng)度為 2048 位
- G404:不安全的隨機(jī)數(shù)源(rand)
- G501:導(dǎo)入黑名單列表:crypto/md5
- G502:導(dǎo)入黑名單列表:crypto/des
- G503:導(dǎo)入黑名單列表:crypto/rc4
- G504:導(dǎo)入黑名單列表:net/http/cgi
- G505:導(dǎo)入黑名單列表:crypto/sha1
- G601: 在 range 語(yǔ)句中使用隱式的元素別名
排除指定的測(cè)試
你可以自定義 gosec 來(lái)避免對(duì)已知為安全的問(wèn)題進(jìn)行掃描和報(bào)告。你可以使用 -exclude 選項(xiàng)和上面的規(guī)則編號(hào)來(lái)忽略指定的問(wèn)題。
例如,如果你不想讓 gosec 檢查源碼中硬編碼憑證相關(guān)的未處理的錯(cuò)誤,那么你可以運(yùn)行下面的命令來(lái)忽略這些錯(cuò)誤:
- $ gosec -exclude=G104 ./...
- $ gosec -exclude=G104,G101 ./...
有時(shí)候你知道某段代碼是安全的,但是 gosec 還是會(huì)報(bào)出問(wèn)題。然而,你又不想完全排除掉整個(gè)檢查,因?yàn)槟阆胱?gosec 檢查新增的代碼。通過(guò)在你已知為安全的代碼塊添加 #nosec 標(biāo)記可以避免 gosec 掃描。這樣 gosec 會(huì)繼續(xù)掃描新增代碼,而忽略掉 #nosec 標(biāo)記的代碼塊。
運(yùn)行指定的檢查
另一方面,如果你只想檢查指定的問(wèn)題,你可以通過(guò) -include 選項(xiàng)和規(guī)則編號(hào)來(lái)告訴 gosec 運(yùn)行哪些檢查:
- $ gosec -include=G201,G202 ./...
掃描測(cè)試文件
Go 語(yǔ)言自帶對(duì)測(cè)試的支持,通過(guò)單元測(cè)試來(lái)檢驗(yàn)一個(gè)元素是否符合預(yù)期。在默認(rèn)模式下,gosec 會(huì)忽略測(cè)試文件,你可以使用 -tests 選項(xiàng)把它們包含進(jìn)來(lái):
- gosec -tests ./...
修改輸出的格式
找出問(wèn)題只是它的一半功能;另一半功能是把它檢查到的問(wèn)題以用戶友好同時(shí)又方便工具處理的方式報(bào)告出來(lái)。幸運(yùn)的是,gosec 可以用不同的方式輸出。例如,如果你想看 JSON 格式的報(bào)告,那么就使用 -fmt 選項(xiàng)指定 JSON 格式并把結(jié)果保存到 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 []byte\n305: \theader := *(*reflect.SliceHeader)(unsafe.Pointer(\u0026buf))\n306: \theader. Len *= 8\n",
- "line": "305",
- "column": "36"
- },
用 gosec 檢查容易被發(fā)現(xiàn)的問(wèn)題
靜態(tài)檢查工具不能完全代替人工代碼審計(jì)。然而,當(dāng)代碼量變大、有眾多開(kāi)發(fā)者時(shí),這樣的工具往往有助于以可重復(fù)的方式找出容易被發(fā)現(xiàn)的問(wèn)題。它對(duì)于幫助新開(kāi)發(fā)者識(shí)別和在編碼時(shí)避免引入這些安全缺陷很有用。