Obsah
1. Kontrola potenciálních chyb ve zdrojových kódech nástroji gosec a go-critic
3. První demonstrační příklad s několika problematickými rysy
4. Výsledky analýzy zdrojového kódu prvního demonstračního příkladu nástrojem gosec
5. Označení bloků či jednotlivých příkazů, u kterých se mají vybrané problémy ignorovat
6. Druhý demonstrační příklad s několika problematickými rysy
7. Výsledky analýzy zdrojového kódu druhého demonstračního příkladu nástrojem gosec
8. Třetí demonstrační příklad s několika problematickými rysy
9. Výsledky analýzy zdrojového kódu třetího demonstračního příkladu nástrojem gosec
10. Použití nástroje go-critic
11. Čtvrtý demonstrační příklad s několika problematickými rysy
12. Výsledky analýzy zdrojového kódu čtvrtého demonstračního příkladu nástrojem go-critic
13. Pátý demonstrační příklad s několika problematickými rysy s výsledkem jeho analýzy
14. Šestý demonstrační příklad s několika problematickými rysy s výsledkem jeho analýzy
15. Příklad obsahující problematické části detekované oběma nástroji
16. Kód, který nebude vykonán optimálně
17. Kontrola zdrojových kódů knihoven jazyka Go nástrojem go-critic
19. Repositář s demonstračními příklady
1. Kontrola potenciálních chyb ve zdrojových kódech nástroji gosec a go-critic
Samotný programovací jazyk Go je navržen velmi konzervativně, což bylo ostatně patrné i z článku o (ne)používaní generických datových typů, funkcí a metod (což se pravděpodobně změní v Go 2, ostatně si můžete nové vlastnosti vyzkoušet již v Go 1.18 Beta). To pochopitelně některým vývojářům nemusí vyhovovat, na čemž ale ve skutečnosti nemusí být vůbec nic špatného – ideální univerzálně přijímaný programovací jazyk totiž neexistoval, neexistuje a pravděpodobně ani nikdy existovat nebude, protože některé vlastnosti jazyků jsou protichůdné. Ovšem samotný programovací jazyk je jen jednou (i když pochopitelně velmi důležitou) součástí celého ekosystému, který kromě překladače (někdy interpretru) obsahuje i vývojová prostředí a ladicí nástroje, ale i další pomocné nástroje a utility. Mezi tyto nástroje patří i utility určené pro kontrolu kvality zdrojových kódů, odhalování různých chyb nerozpoznaných překladačem, potenciálních chyb, špatně strukturovaného kódu, nedodržování zavedených idiomů atd. Jedním z těchto nástrojů je go-critic, který si dnes popíšeme; další nástroje, i když úžeji zaměřené, již byly popsány v šedesáté části seriálu o Go.
Samostatnou kapitolu tvoří nástroje sloužící k odhalení potenciálních bezpečnostních problémů. I těchto nástrojů existuje relativně velké množství a jedním z nejdůležitějších projektů (navíc stále aktivně vyvíjeným) z této skupiny – nástrojem gosec – se budeme zabývat dnes.
2. Použití nástroje gosec
V první polovině dnešního článku se budeme zabývat možnostmi, které nám nabízí nástroj nazvaný příznačně gosec. Tento nástroj dokáže najít ve zdrojových kódech potenciální bezpečnostní problémy. Například se to týká konstrukce cest k souborům na základě „podivně“ získaných údajů (třeba přes REST API), skládání SQL dotazů, přímé použití tokenů v programovém kódu (i v testech), popř. použití algoritmů, které již dnes nejsou považovány za bezpečné. Typickým příkladem takového algoritmu je MD5.
Instalace nástroje gosec je přímočará:
$ go install github.com/securego/gosec/v2/cmd/gosec@latest
Pro lepší představu o možnostech nástroje gosec jsou pod tímto odstavcem vypsána všechna pravidla aplikovaná na zdrojové kódy. Tato pravidla jsou určena pro hledání potenciálních bezpečnostních chyb:
Pravidlo | Popis pravidla |
---|---|
G101 | Look for hard coded credentials |
G102 | Bind to all interfaces |
G103 | Audit the use of unsafe block |
G104 | Audit errors not checked |
G106 | Audit the use of ssh.InsecureIgnoreHostKey |
G107 | Url provided to HTTP request as taint input |
G108 | Profiling endpoint automatically exposed on /debug/pprof |
G109 | Potential Integer overflow made by strconv.Atoi result conversion to int16/32 |
G110 | Potential DoS vulnerability via decompression bomb |
G201 | SQL query construction using format string |
G202 | SQL query construction using string concatenation |
G203 | Use of unescaped data in HTML templates |
G204 | Audit use of command execution |
G301 | Poor file permissions used when creating a directory |
G302 | Poor file permissions used with chmod |
G303 | Creating tempfile using a predictable path |
G304 | File path provided as taint input |
G305 | File traversal when extracting zip/tar archive |
G306 | Poor file permissions used when writing to a new file |
G307 | Deferring a method which returns an error |
G401 | Detect the usage of DES, RC4, MD5 or SHA1 |
G402 | Look for bad TLS connection settings |
G403 | Ensure minimum RSA key length of 2048 bits |
G404 | Insecure random number source (rand) |
G501 | Import blocklist: crypto/md5 |
G502 | Import blocklist: crypto/des |
G503 | Import blocklist: crypto/rc4 |
G504 | Import blocklist: net/http/cgi |
G505 | Import blocklist: crypto/sha1 |
G601 | Implicit memory aliasing of items from a range statement |
Některé typické problémy, které lze nalézt ve zdrojových kódech reálných projektů, budou ukázány v navazujících kapitolách.
3. První demonstrační příklad s několika problematickými rysy
Pro zjištění některých vlastností nástroje gosec i chyb, resp. spíše řečeno potenciálních chyb, které dokáže detekovat, použijeme následující demonstrační příklad, který byl získán ze skutečného projektu (a do značné míry byl zkrácen). Funkce readPipelineLogFile má sloužit pro načtení logovacích informací, přičemž každý řádek v logu obsahuje datovou strukturu PipelineLogEntry uloženou ve formátu JSON:
package main import ( "bufio" "encoding/json" "log" "os" ) // PipelineLogEntry represents one log entry (record) read from log file. type PipelineLogEntry struct { Level string `json:"levelname"` Time string `json:"asctime"` Name string `json:"name"` Filename string `json:"filename"` Message string `json:"message"` } func readPipelineLogFile(filename string) ([]PipelineLogEntry, error) { entries := []PipelineLogEntry{} file, err := os.Open(filename) if err != nil { return entries, err } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { entry := PipelineLogEntry{} err = json.Unmarshal([]byte(scanner.Text()), &entry) if err != nil { log.Println(err) } else { entries = append(entries, entry) } } if err := scanner.Err(); err != nil { return entries, err } return entries, nil } func main() { readPipelineLogFile("foobar") }
Zdrojový kód tohoto příkladu získáte na adrese https://github.com/tisnik/wccode/blob/master/gosec_issues1.go.
4. Výsledky analýzy zdrojového kódu prvního demonstračního příkladu nástrojem gosec
Spuštění analýzy nástrojem gosec je snadná – pouze se tomuto nástroji předá název balíčku, popř. „./…“ (bez uvozovek) pro kontrolu všech balíčků umístěných v aktuálním adresáři či podadresářích:
$ gosec ./...
V případě, že máte nainstalovánu poslední verzi nástroje gosec, měly by výsledky analýzy zdrojového kódu z předchozí kapitoly vypadat takto:
[gosec] 2021/12/13 13:02:14 Including rules: default [gosec] 2021/12/13 13:02:14 Excluding rules: default [gosec] 2021/12/13 13:02:14 Import directory: /home/ptisnovs/temp/z [gosec] 2021/12/13 13:02:14 Checking package: main [gosec] 2021/12/13 13:02:14 Checking file: /home/ptisnovs/temp/z/gosec_issues_1.go Results: [/home/ptisnovs/temp/z/gosec_issues_1.go:22] - G304 (CWE-22): Potential file inclusion via variable (Confidence: HIGH, Severity: MEDIUM) 21: > 22: file, err := os.Open(filename) 23: if err != nil { [/home/ptisnovs/temp/z/gosec_issues_1.go:27] - G307 (CWE-703): Deferring unsafe method "Close" on type "*os.File" (Confidence: HIGH, Severity: MEDIUM) 26: > 27: defer file.Close() 28: [/home/ptisnovs/temp/z/gosec_issues_1.go:48] - G104 (CWE-703): Errors unhandled. (Confidence: HIGH, Severity: LOW) 47: func main() { > 48: readPipelineLogFile("foobar") 49: } Summary: Gosec : dev Files : 1 Lines : 49 Nosec : 0 Issues : 3
Alternativně je možné si vyžádat výstup ve formátu JSON, který je snáze strojově zpracovatelný:
$ gosec -fmt=json -out=report.json ./...
V tomto případě bude výsledek vypadat následovně:
{ "Golang errors": {}, "Issues": [ { "severity": "MEDIUM", "confidence": "HIGH", "cwe": { "id": "22", "url": "https://cwe.mitre.org/data/definitions/22.html" }, "rule_id": "G304", "details": "Potential file inclusion via variable", "file": "/home/ptisnovs/temp/z/gosec_issues_1.go", "code": "21: \n22: \tfile, err := os.Open(filename)\n23: \tif err != nil {\n", "line": "22", "column": "15", "nosec": false, "suppressions": null }, { "severity": "MEDIUM", "confidence": "HIGH", "cwe": { "id": "703", "url": "https://cwe.mitre.org/data/definitions/703.html" }, "rule_id": "G307", "details": "Deferring unsafe method \"Close\" on type \"*os.File\"", "file": "/home/ptisnovs/temp/z/gosec_issues_1.go", "code": "26: \n27: \tdefer file.Close()\n28: \n", "line": "27", "column": "2", "nosec": false, "suppressions": null }, { "severity": "LOW", "confidence": "HIGH", "cwe": { "id": "703", "url": "https://cwe.mitre.org/data/definitions/703.html" }, "rule_id": "G104", "details": "Errors unhandled.", "file": "/home/ptisnovs/temp/z/gosec_issues_1.go", "code": "47: func main() {\n48: \treadPipelineLogFile(\"foobar\")\n49: }\n", "line": "48", "column": "2", "nosec": false, "suppressions": null } ], "Stats": { "files": 1, "lines": 49, "nosec": 0, "found": 3 }, "GosecVersion": "dev" }
Podívejme se nyní na jednotlivé problémy, které byly detekovány:
[/home/ptisnovs/temp/z/gosec_issues_1.go:22] - G304 (CWE-22): Potential file inclusion via variable (Confidence: HIGH, Severity: MEDIUM) 21: > 22: file, err := os.Open(filename) 23: if err != nil {
Toto je obecný problém se střední závažností, který souvisí s tím, že funkci lze zavolat s libovolným řetězcem, který je použit jako název souboru. Pokud by byl řetězec získán způsobem, jenž je dostupný potenciálnímu útočníkovi (například z REST API), mohlo by to vést k závažnějším komplikacím.
[/home/ptisnovs/temp/z/gosec_issues_1.go:27] - G307 (CWE-703): Deferring unsafe method "Close" on type "*os.File" (Confidence: HIGH, Severity: MEDIUM) 26: > 27: defer file.Close() 28:
Tento problém má opět střední závažnost a souvisí s tím, že se soubor uzavírá až v bloku defer, což je v některých případech příliš pozdě. Tento problém je velmi pěkně popsán v článku Don't defer Close() on writable files. Tomuto zajímavému problému se budeme věnovat v samostatném textu.
[/home/ptisnovs/temp/z/gosec_issues_1.go:48] - G104 (CWE-703): Errors unhandled. (Confidence: HIGH, Severity: LOW) 47: func main() { > 48: readPipelineLogFile("foobar") 49: }
Třetí potenciální problém je detekovatelný i dalšími nástroji – nekontrolujeme návratovou chybovou hodnotu. Jedná se o podobný antipattern, jakým je použití prázdného bloku catch nebo except v jazycích podporujících práci s výjimkami.
5. Označení bloků či jednotlivých příkazů, u kterých se mají vybrané problémy ignorovat
V některých situacích ovšem skutečně potřebujeme (například) vytvořit jméno souboru z nekonstantních řetězců (řetězcových literálů); podobně jako se někdy (!) skládají či formátují SQL dotazy. V takových případech by bylo vhodné mít možnost označit příslušné příkazy nebo bloky, aby je nástroj gosec ignoroval. To je skutečně možné. K tomuto účelu se používají komentáře obsahující slovo „gosec“, za kterým následuje jméno pravidla, které chceme zakázat. Tento komentář může být přidán k příkazu, bloku, před volání funkce atd. Podívejme se na příklad použití – viz podtržené části zdrojového kódu:
package main import ( "bufio" "encoding/json" "log" "os" ) // PipelineLogEntry represents one log entry (record) read from log file. type PipelineLogEntry struct { Level string `json:"levelname"` Time string `json:"asctime"` Name string `json:"name"` Filename string `json:"filename"` Message string `json:"message"` } func readPipelineLogFile(filename string) ([]PipelineLogEntry, error) { entries := []PipelineLogEntry{} file, err := os.Open(filename) // #nosec G304 if err != nil { return entries, err } // #nosec G307 defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { entry := PipelineLogEntry{} err = json.Unmarshal([]byte(scanner.Text()), &entry) if err != nil { log.Println(err) } else { entries = append(entries, entry) } } if err := scanner.Err(); err != nil { return entries, err } return entries, nil } func main() { // #nosec G104 readPipelineLogFile("foobar") }
Zdrojový kód tohoto příkladu získáte na adrese https://github.com/tisnik/wccode/blob/master/gosec_issues1_nosec.go.
Nyní by již kontrola neměla odhalit žádné problémy:
$ gosec ./... [gosec] 2021/12/14 12:37:03 Including rules: default [gosec] 2021/12/14 12:37:03 Excluding rules: default [gosec] 2021/12/14 12:37:03 Import directory: /home/ptisnovs/temp/z [gosec] 2021/12/14 12:37:03 Checking package: main [gosec] 2021/12/14 12:37:03 Checking file: /home/ptisnovs/temp/z/gosec_issues_1_nosec.go Results: Summary: Gosec : dev Files : 1 Lines : 51 Nosec : 3 Issues : 0
Zákaz aplikace pravidel komentářem „// gosec“ lze ovšem zakázat (pokud například nedůvěřujete určitému projektu):
$ gosec -nosec ./...
[gosec] 2021/12/14 12:37:31 Including rules: default [gosec] 2021/12/14 12:37:31 Excluding rules: default [gosec] 2021/12/14 12:37:31 Import directory: /home/ptisnovs/temp/z [gosec] 2021/12/14 12:37:31 Checking package: main [gosec] 2021/12/14 12:37:31 Checking file: /home/ptisnovs/temp/z/gosec_issues_1_nosec.go Results: [/home/ptisnovs/temp/z/gosec_issues_1_nosec.go:22] - G304 (CWE-22): Potential file inclusion via variable (Confidence: HIGH, Severity: MEDIUM) 21: > 22: file, err := os.Open(filename) // #nosec G304 23: if err != nil { [/home/ptisnovs/temp/z/gosec_issues_1_nosec.go:28] - G307 (CWE-703): Deferring unsafe method "Close" on type "*os.File" (Confidence: HIGH, Severity: MEDIUM) 27: // #nosec G307 > 28: defer file.Close() 29: [/home/ptisnovs/temp/z/gosec_issues_1_nosec.go:50] - G104 (CWE-703): Errors unhandled. (Confidence: HIGH, Severity: LOW) 49: // #nosec G104 > 50: readPipelineLogFile("foobar") 51: } Summary: Gosec : dev Files : 1 Lines : 51 Nosec : 0 Issues : 3
6. Druhý demonstrační příklad s několika problematickými rysy
Ve druhé kapitole jsme si řekli, že nástroj gosec dokáže zjistit i použití algoritmů, které již nejsou považovány za bezpečné. Konkrétně se jedná o následující pravidla:
Pravidlo | Popis pravidla |
---|---|
G401 | Detect the usage of DES, RC4, MD5 or SHA1 |
G403 | Ensure minimum RSA key length of 2048 bits |
G404 | Insecure random number source (rand) |
G501 | Import blocklist: crypto/md5 |
G502 | Import blocklist: crypto/des |
G503 | Import blocklist: crypto/rc4 |
G504 | Import blocklist: net/http/cgi |
G505 | Import blocklist: crypto/sha1 |
Vlastnosti gosec v této oblasti si ověříme následujícím, tentokrát velmi krátkým kódem:
package main import ( "crypto/md5" "fmt" "io" ) func main() { hash := md5.New() io.WriteString(hash, "Příliš žluťoučký kůň") fmt.Printf("%x", hash.Sum(nil)) }
Zdrojový kód tohoto příkladu získáte na adrese https://github.com/tisnik/wccode/blob/master/gosec_issues2.go.
7. Výsledky analýzy zdrojového kódu druhého demonstračního příkladu nástrojem gosec
Opět se podívejme na to, jaké výsledky získáme analýzou zdrojového kódu představeného v předchozí kapitole. Spustíme nástroj gosec s parametrem „./…“:
$ gosec ./...
V případě, že je nainstalována poslední verze nástroje gosec, vypíše se trojice potenciálních problémů:
Results: [/home/ptisnovs/temp/z/gosec_issues_2.go:10] - G401 (CWE-326): Use of weak cryptographic primitive (Confidence: HIGH, Severity: MEDIUM) 9: func main() { > 10: hash := md5.New() 11: [/home/ptisnovs/temp/z/gosec_issues_2.go:4] - G501 (CWE-327): Blocklisted import crypto/md5: weak cryptographic primitive (Confidence: HIGH, Severity: MEDIUM) 3: import ( > 4: "crypto/md5" 5: "fmt" [/home/ptisnovs/temp/z/gosec_issues_2.go:12] - G104 (CWE-703): Errors unhandled. (Confidence: HIGH, Severity: LOW) 11: > 12: io.WriteString(hash, "Příliš žluťoučký kůň") 13: fmt.Printf("%x", hash.Sum(nil)) Summary: Gosec : dev Files : 1 Lines : 14 Nosec : 0 Issues : 3
Všechny tři potenciální problémy jsou snadno pochopitelné – zjistilo se, že se importuje a dokonce i používá algoritmus MD5 a navíc, že není provedena kontrola, zda nedošlo k nějaké chybě při volání funkce io.WriteString (což popravdě řečeno ignoruje relativně velká část programů).
Alternativně pochopitelně opět můžeme žádat výstup ve formátu strojově zpracovatelného formátu JSON, a to nepatrně upraveným zavoláním:
$ gosec -fmt=json -out=report.json ./...
S výsledkem:
{ "Golang errors": {}, "Issues": [ { "severity": "MEDIUM", "confidence": "HIGH", "cwe": { "id": "326", "url": "https://cwe.mitre.org/data/definitions/326.html" }, "rule_id": "G401", "details": "Use of weak cryptographic primitive", "file": "/home/ptisnovs/temp/z/gosec_issues_2.go", "code": "9: func main() {\n10: \thash := md5.New()\n11: \n", "line": "10", "column": "10", "nosec": false, "suppressions": null }, { "severity": "MEDIUM", "confidence": "HIGH", "cwe": { "id": "327", "url": "https://cwe.mitre.org/data/definitions/327.html" }, "rule_id": "G501", "details": "Blocklisted import crypto/md5: weak cryptographic primitive", "file": "/home/ptisnovs/temp/z/gosec_issues_2.go", "code": "3: import (\n4: \t\"crypto/md5\"\n5: \t\"fmt\"\n", "line": "4", "column": "2", "nosec": false, "suppressions": null }, { "severity": "LOW", "confidence": "HIGH", "cwe": { "id": "703", "url": "https://cwe.mitre.org/data/definitions/703.html" }, "rule_id": "G104", "details": "Errors unhandled.", "file": "/home/ptisnovs/temp/z/gosec_issues_2.go", "code": "11: \n12: \tio.WriteString(hash, \"Příliš žluťoučký kůň\")\n13: \tfmt.Printf(\"%x\", hash.Sum(nil))\n", "line": "12", "column": "2", "nosec": false, "suppressions": null } ], "Stats": { "files": 1, "lines": 14, "nosec": 0, "found": 3 }, "GosecVersion": "dev" }
Jen pro zajímavost si ukažme, jak se jednotlivá pravidla zakážou – nyní pro celý program, popř. pro příkaz import:
// #nosec G401 // #nosec G104 package main import ( "crypto/md5" // #nosec G501 "fmt" "io" ) func main() { hash := md5.New() io.WriteString(hash, "Příliš žluťoučký kůň") fmt.Printf("%x", hash.Sum(nil)) }
Další kontrola již proběhne bez detekce chyb:
[gosec] 2021/12/14 12:38:39 Including rules: default [gosec] 2021/12/14 12:38:39 Excluding rules: default [gosec] 2021/12/14 12:38:39 Import directory: /home/ptisnovs/temp/z [gosec] 2021/12/14 12:38:39 Checking package: main [gosec] 2021/12/14 12:38:39 Checking file: /home/ptisnovs/temp/z/gosec_issues_2_nosec.go Results: Summary: Gosec : dev Files : 1 Lines : 16 Nosec : 2 Issues : 0
8. Třetí demonstrační příklad s několika problematickými rysy
Třetí demonstrační příklad, který si dnes ukážeme a který budeme analyzovat, není stoprocentně založen na reálných zdrojových kódech, ovšem ukazuje, jak nástroj gosec dokáže odhalit potenciální problémy, které vznikají při skládání SQL dotazů. Předchozí verze nástroje gosec varovaly před jakýmkoli skládáním SQL z nekonstantních řetězců, nová verze je ovšem již chytřejší a některé operace již ignoruje. Nicméně se podívejme na příklad s několika SQL dotazy:
package main import ( "database/sql" "fmt" "os" ) func foo(arg string) { db, err := sql.Open("sqlite3", ":memory:") if err != nil { panic(err) } rows, err := db.Query("SELECT * FROM foo WHERE name = " + arg) if err != nil { panic(err) } defer rows.Close() } func bar(arg string) { db, err := sql.Open("sqlite3", ":memory:") if err != nil { panic(err) } query := fmt.Sprintf("select * from foo where name = '%s'", arg) rows, err := db.Query(query) if err != nil { panic(err) } defer rows.Close() } func main() { foo("foo") bar("bar") }
Zdrojový kód tohoto příkladu získáte na adrese https://github.com/tisnik/wccode/blob/master/gosec_issues3.go.
9. Výsledky analýzy zdrojového kódu třetího demonstračního příkladu nástrojem gosec
Zjištění potenciálních problémů ve zdrojovém kódu opět zajistí tento příkaz:
$ gosec ./...
S výsledky:
Results: Golang errors in file: [/home/ptisnovs/temp/z/gocritic_gosec_issues.go]: > [line 6 : column 2] - "os" imported but not used [/home/ptisnovs/temp/z/gocritic_gosec_issues.go:28] - G201 (CWE-89): SQL string formatting (Confidence: HIGH, Severity: MEDIUM) 27: > 28: query := fmt.Sprintf("select * from foo where name = '%s'", arg) 29: [/home/ptisnovs/temp/z/gocritic_gosec_issues.go:14] - G202 (CWE-89): SQL string concatenation (Confidence: HIGH, Severity: MEDIUM) 13: } > 14: rows, err := db.Query("SELECT * FROM foo WHERE name = " + arg) 15: Summary: Gosec : dev Files : 1 Lines : 40 Nosec : 0 Issues : 2
Nejzajímavější jsou chyby získané pravidly G201 a G202. První pravidlo zjistilo, že se SQL dotaz získává formátováním řetězce, což může, ale taktéž nemusí být problematické. Druhé pravidlo detekovalo použití argumentu, který je do funkce předán a tedy obecně není možné zaručit, odkud se jeho obsah získá.
Pro úplnost si ještě ukažme výstup z nástroje gosec ve formátu JSON:
{ "Golang errors": { "/home/ptisnovs/temp/z/gocritic_gosec_issues.go": [ { "line": 6, "column": 2, "error": "\"os\" imported but not used" } ] }, "Issues": [ { "severity": "MEDIUM", "confidence": "HIGH", "cwe": { "id": "89", "url": "https://cwe.mitre.org/data/definitions/89.html" }, "rule_id": "G201", "details": "SQL string formatting", "file": "/home/ptisnovs/temp/z/gocritic_gosec_issues.go", "code": "27: \n28: \tquery := fmt.Sprintf(\"select * from foo where name = '%s'\", arg)\n29: \n", "line": "28", "column": "11", "nosec": false, "suppressions": null }, { "severity": "MEDIUM", "confidence": "HIGH", "cwe": { "id": "89", "url": "https://cwe.mitre.org/data/definitions/89.html" }, "rule_id": "G202", "details": "SQL string concatenation", "file": "/home/ptisnovs/temp/z/gocritic_gosec_issues.go", "code": "13: \t}\n14: \trows, err := db.Query(\"SELECT * FROM foo WHERE name = \" + arg)\n15: \n", "line": "14", "column": "24", "nosec": false, "suppressions": null } ], "Stats": { "files": 1, "lines": 40, "nosec": 0, "found": 2 }, "GosecVersion": "dev" }
10. Použití nástroje go-critic
„There is never too much static code analysis. Try it out.“
Nástroj gosec, jehož základní vlastnosti jsme si ukázali v předchozích kapitolách, je striktně určen pro hledání potenciálních bezpečnostních problémů ve zdrojových kódech. Druhý dnes představený nástroj se jmenuje go-critic a jeho zaměření je mnohem větší, protože slouží jak pro hledání reálných i potenciálních chyb, tak i těch částí kódu, které nemusí být vykonány efektivně. Taktéž ovšem slouží pro detekci kódu, který není napsán idiomaticky. Všechna pravidla, resp. všechny (potenciální) problémy, které tento nástroj dokáže detekovat, jsou vypsány na stránce Checks overview.
Oba dva zmíněné nástroje ovšem mají společný základ, protože provádí statickou analýzu kódu na základě zkonstruovaného AST.
Instalace nástroje go-critic je triviální:
$ GO111MODULE=on go get -v -u github.com/go-critic/go-critic/cmd/gocritic
11. Čtvrtý demonstrační příklad s několika problematickými rysy
Ve čtvrtém demonstračním příkladu, který vypadá zdánlivě zcela v pořádku, je naschvál ponecháno několik problematických rysů, které budou nástrojem go-critic odhaleny:
package main import "fmt" import "strings" func printMessages(Format string, message1 string, message2 string) { //fmt.Printf("%s %s\n", message1, message2) if len(message1) != 0 && len(message2) != 0 { fmt.Printf(Format, strings.Replace(message1, " ", "", -1), message2) } } func main() { const fmt = "%s %s\n" for i := 0; 10 > i; i = i + 1 { printMessages(fmt, "Hello ", "world") } }
Zdrojový kód tohoto demonstračního příkladu získáte na adrese https://github.com/tisnik/wccode/blob/master/gocritic_issues1.go.
12. Výsledky analýzy zdrojového kódu čtvrtého demonstračního příkladu nástrojem go-critic
Nástroj gocritic se spouští s odlišnými parametry. Především je nutné předat parametr/příkaz check a následně lze s využitím parametrů -enable a -disable určit, která pravidla se mají použít a která popř. ne. Můžeme dokonce povolit všechna pravidla s využitím přepínače -enableAll:
$ gocritic check -enableAll ./...
Výsledky pro demonstrační příklad z předchozí kapitoly budou vypadat takto:
./gocritic_issues_1.go:17:22: assignOp: replace `i = i + 1` with `i++` ./gocritic_issues_1.go:6:20: captLocal: `Format' should not be capitalized ./gocritic_issues_1.go:7:2: commentFormatting: put a space between `//` and comment text ./gocritic_issues_1.go:7:2: commentedOutCode: may want to remove commented-out code ./gocritic_issues_1.go:9:5: emptyStringTest: replace `len(message1) != 0` with `message1 != ""` ./gocritic_issues_1.go:9:27: emptyStringTest: replace `len(message2) != 0` with `message2 != ""` ./gocritic_issues_1.go:15:8: importShadow: shadow of imported package 'fmt' ./gocritic_issues_1.go:6:1: paramTypeCombine: func(Format string, message1 string, message2 string) could be replaced with func(Format, message1, message2 string) ./gocritic_issues_1.go:10:22: wrapperFunc: use strings.ReplaceAll method in `strings.Replace(message1, " ", "", -1)` ./gocritic_issues_1.go:17:14: yodaStyleExpr: consider to change order in expression to i <= 10
Většina problémů, resp. určitých nedostatků nalezených v kódu (a to velmi krátkém!) je snadno pochopitelná:
- Náhrada složitého výrazu i = i + 1 za idiomatičtější i++
- Parametr je vždy lokální a tudíž by měl začínat velkým písmenem
- Za znakem uvozujícím komentář se píše mezera
- Detekce zakomentovaného kódu – ten nemá co v repositáři pohledávat :-)
- Zajímavá a užitečná je detekce neidiomatického testu na prázdný řetězec – emptyStringTest
- Dále je konstanta, proměnná či parametr pojmenován stejně jako importovaný balíček – což se mi popravdě stává prakticky neustále
- Detekce, že parametry se stejným typem mohou mít uveden typ společně (a to je zrovna u hlaviček funkcí užitečné)
- Další velmi užitečná je detekce použití zbytečně univerzálních funkcí namísto funkce speciální – strings.Replace/ReplaceAll
- A konečně použití podmínky 10 > i namísto přece jen čitelnější varianty i < = 10
13. Pátý demonstrační příklad s několika problematickými rysy s výsledkem jeho analýzy
Další demonstrační příklad obsahuje velmi zajímavou chybu (která se tam navíc může dostat kdykoli později). Prozatím nebudu prozrazovat jakou – nejprve se na zdrojový kód příkladu podívejte; výsledky analýzy budou uvedeny až pod zdrojovým kódem:
package main import ( "bufio" "encoding/json" "log" "os" ) // PipelineLogEntry represents one log entry (record) read from log file. type PipelineLogEntry struct { Level string `json:"levelname"` Time string `json:"asctime"` Name string `json:"name"` Filename string `json:"filename"` Message string `json:"message"` } func readPipelineLogFile(filename string) ([]PipelineLogEntry, error) { entries := []PipelineLogEntry{} file, err := os.Open(filename) if err != nil { return entries, err } defer func() { err := file.Close() if err != nil { log.Println(err) } }() scanner := bufio.NewScanner(file) for scanner.Scan() { entry := PipelineLogEntry{} err = json.Unmarshal([]byte(scanner.Text()), &entry) if err != nil { log.Fatal(err) } else { entries = append(entries, entry) } } if err := scanner.Err(); err != nil { return entries, err } return entries, nil } func main() { readPipelineLogFile("foobar") }
Zdrojový kód tohoto demonstračního příkladu získáte na adrese https://github.com/tisnik/wccode/blob/master/gocritic_issues2.go.
Pokusme se nyní zjistit potenciální problémy v kódu tohoto příkladu:
$ gocritic check -enableAll ./...
Výsledkem je detekce chyby, která nemusí být na první pohled viditelná – pokud se totiž zavolá funkce log.Fatal, neprovede se již blok (resp. anonymní funkce) definovaná v defer:
./gocritic_issues_2.go:40:4: exitAfterDefer: log.Fatal will exit, and `defer func(){...}(...)` will not run
14. Šestý demonstrační příklad s několika problematickými rysy s výsledkem jeho analýzy
I další demonstrační příklad vypadá zdánlivě neškodně, ovšem na následujících několika řádcích nalezneme neuvěřitelných devět problematických rysů. Nejdříve se opět podívejme na zdrojový kód tohoto příkladu:
package main import "fmt" func new(len int, cap int) ([]int, int) { vals := make([]int, len) for i := 0; cap > i; i = i + 1 { vals = append(vals, i) vals = append(vals, i*2) } return vals, len + cap } func main() { vals, _ := new(0, 010) fmt.Println(vals) for i := 0; i < len(vals); i++ { vals[i] = 0 } }
Výsledek běhu nástroje go-critic:
./gocritic_issues_3.go:8:3: appendCombine: can combine chain of 2 appends into one ./gocritic_issues_3.go:7:23: assignOp: replace `i = i + 1` with `i++` ./gocritic_issues_3.go:5:10: builtinShadow: shadowing of predeclared identifier: len ./gocritic_issues_3.go:5:19: builtinShadow: shadowing of predeclared identifier: cap ./gocritic_issues_3.go:5:6: builtinShadowDecl: shadowing of predeclared identifier: new ./gocritic_issues_3.go:15:13: octalLiteral: suspicious octal args in `new(0, 010)` ./gocritic_issues_3.go:5:1: paramTypeCombine: func(len int, cap int) ([]int, int) could be replaced with func(len, cap int) ([]int, int) ./gocritic_issues_3.go:17:2: sliceClear: rewrite as for-range so compiler can recognize this pattern ./gocritic_issues_3.go:5:1: unnamedResult: consider giving a name to these results
Některé z potenciálních problémů už jsme mohli vidět v předchozích kapitolách:
- Náhrada složitého výrazu i = i + 1 za idiomatičtější i++
- Dále je konstanta, proměnná či parametr pojmenován stejně jako importovaný balíček – což se mi popravdě stává prakticky neustále
- Detekce, že parametry se stejným typem mohou mít uveden typ společně (a to je zrovna u hlaviček funkcí užitečné)
To jsou však jen triviality. Zajímavé jsou další problematická místa kódu:
- Použití osmičkových hodnot začínajících pouze na nulu a nikoli dvojicí znaků 0o (což je mnohem lepší, protože programátor dává explicitně najevo svoje úmysly). Mimochodem – tyto problémy lze nalézt i přímo ve zdrojových kódech standardní knihovny jazyka Go.
- Pokus o vymazání řezu (nebo pole) počítanou smyčkou for, zatímco překladač dokáže rozeznat a optimalizovat smyčku typu for-each
Zdrojový kód tohoto demonstračního příkladu získáte na adrese https://github.com/tisnik/wccode/blob/master/gocritic_issues3.go.
15. Příklad obsahující problematické části detekované oběma nástroji
Další příklad již známe, protože jsme se s ním setkali v části věnované nástroji gosec. Vyzkoušejme si tedy, jaké potenciální problémy (a zda vůbec jaké) v tomto zdrojovém kódu nalezne go-critic:
package main import ( "database/sql" "fmt" "os" ) func foo(arg string) { db, err := sql.Open("sqlite3", ":memory:") if err != nil { panic(err) } rows, err := db.Query("SELECT * FROM foo WHERE name = " + arg) if err != nil { panic(err) } defer rows.Close() } func bar(arg string) { db, err := sql.Open("sqlite3", ":memory:") if err != nil { panic(err) } query := fmt.Sprintf("select * from foo where name = '%s'", arg) rows, err := db.Query(query) if err != nil { panic(err) } defer rows.Close() } func main() { foo("foo") bar("bar") }
Výsledek možná není překvapující – používá se defer přímo na konci funkce, což je zbytečné (i když touto konstrukcí může programátor naznačovat, kdy se má příkaz vykonat):
./gocritic_gosec_issues.go:19:2: unnecessaryDefer: defer rows.Close() is placed just before return ./gocritic_gosec_issues.go:34:2: unnecessaryDefer: defer rows.Close() is placed just before return
16. Kód, který nebude vykonán optimálně
Užitečné mohou být i informace o tom, že nějaká část programového kódu nebude vykonána optimálně. Můžeme si to ostatně velmi snadno otestovat. Předpokládejme, že v programu pracujeme s touto jednoduchou datovou strukturou:
// KafkaMessageLogEntry represents one log entry (record) read from log file. type KafkaMessageLogEntry struct { Level string `json:"level"` Time string `json:"time"` Message string `json:"message"` Type string `json:"type"` Error string `json:"error"` Topic string `json:"topic"` Offset int `json:"offset"` Group string `json:"group"` Organization int `json:"organization"` Cluster string `json:"cluster"` }
Funkce printReadEntry bude akceptovat tuto datovou strukturu jako parametr:
func printReadEntry(entry KafkaMessageLogEntry) { fmt.Printf("%s %s %s %d %d %s\n", entry.Time, entry.Group, entry.Topic, entry.Offset, entry.Organization, entry.Cluster) }
To ovšem nemusí být optimální, protože nástroj go-critic odvodil, že při každém volání této funkce se bude muset přesunout 144 bajtů na zásobník, takže by mohlo být lepší pouze předat ukazatel na strukturu:
./temp/analyser.go:167:21: hugeParam: entry is heavy (144 bytes); consider passing it by pointer
V další funkci je detekováno, že se v každé iteraci oněch 144 bajtů zkopíruje do lokální proměnné entry, což opět není optimální:
func filterConsumedMessages(entries []KafkaMessageLogEntry) []KafkaMessageLogEntry { consumed := []KafkaMessageLogEntry{} for _, entry := range entries { if entry.Message == "Consumed" && entry.Group != "" { consumed = append(consumed, entry) } } return consumed }
Výsledek detekce:
./temp/analyser.go:124:2: rangeValCopy: each iteration copies 144 bytes (consider pointers or indexing)
Řešení by mohlo spočívat v tom, že by se vytvořila proměnná typu ukazatel na strukturu – v tomto ohledu bohužel jazyk Go neposkytuje lepší prostředky jak určit, že se sice má iterovat přes hodnoty v poli/řezu, ale stačí nám získat pouze ukazatel na prvek.
17. Kontrola zdrojových kódů knihoven jazyka Go nástrojem go-critic
Pro zajímavost se můžeme pokusit o provedení kontroly zdrojových kódů dodávaných přímo s programovacím jazykem Go. Po instalaci Go se totiž v podadresáři src nachází mj. i zdrojové kódy ke standardním knihovnám, a to včetně jednotkových testů. Analýza s využitím nástrojů gosec a go-critic sice bude nějakou dobu trvat, ale uvidíme, že i přes velký rozsah těchto kódů (necelé dva miliony řádků) je nalezeno jen minimum potenciálních problémů – a to navíc mnohdy „pouze“ v testech, v nichž se někdy musí ohýbat pravidla jak psát efektivní, idiomatický a korektní kód.
18. Statistika na závěr
Zdrojové kódy uložené v adresáři src (standardní, dnes již poněkud zastaralá instalace Go 1.17.1) mají 1903007 řádků a nástroj go-critic v nich nalezl pouze 2861 potenciálních problémů, většinou ovšem jen netypicky zapsaných výrazů. Celou statistiku je možné z nalezených problémů vygenerovat tímto jednoduchým skriptem:
from collections import Counter counter = Counter() with open("results.txt") as fin: for i, line in enumerate(fin): type = line.split(" ")[1][:-1] counter[type] += 1 for cnt, type in counter.most_common(30): print(cnt, type)
Následuje tabulka s třiceti nejčastějšími typy problémů. Povšimněte si, že se skutečně většinou jedná o „otočené“ operandy, parametry, jejichž typy lze zapsat jen jednou, popř. o detekci toho, že interní identifikátor se jmenuje stejně jako importovaný balíček:
Test/problém | Počet případů |
---|---|
yodaStyleExpr | 358 |
paramTypeCombine | 237 |
importShadow | 208 |
unnamedResult | 207 |
commentedOutCode | 166 |
builtinShadow | 166 |
ifElseChain | 145 |
typeUnparen | 128 |
emptyStringTest | 127 |
singleCaseSwitch | 93 |
octalLiteral | 85 |
captLocal | 83 |
assignOp | 69 |
hugeParam | 64 |
commentFormatting | 64 |
exitAfterDefer | 59 |
preferStringWriter | 58 |
redundantSprint | 36 |
unslice | 34 |
initClause | 33 |
elseif | 32 |
sloppyReassign | 31 |
filepathJoin | 26 |
httpNoBody | 23 |
preferWriteByte | 22 |
unlambda | 21 |
dupImport | 18 |
ptrToRefParam | 17 |
appendCombine | 16 |
nestingReduce | 16 |
19. Repositář s demonstračními příklady
Zdrojové kódy všech minule i dnes použitých demonstračních příkladů byly uloženy do staronového Git repositáře, který je dostupný na adrese https://github.com/tisnik/wccode (stále na GitHubu :-). V případě, že nebudete chtít klonovat celý repositář (ten je ovšem – alespoň prozatím – velmi malý, dnes má přibližně stovku kilobajtů), můžete namísto toho použít odkazy na jednotlivé demonstrační příklady, které naleznete v následující tabulce:
# | Příklad/soubor | Stručný popis | Cesta |
---|---|---|---|
1 | 01_missing_package.go | nekorektní zdrojový kód, v němž chybí deklarace balíčku | https://github.com/tisnik/wccode/blob/master/01_missing_package.go |
2 | 02_parenthesis.go | chybně umístěná otevírací bloková závorka | https://github.com/tisnik/wccode/blob/master/02_parenthesis.go |
3 | 03_bad_syntax.go | chybně umístěné otevírací i uzavírací blokové závorky | https://github.com/tisnik/wccode/blob/master/03_bad_syntax.go |
4 | 04_before_transform.go | zdrojový kód před transformací nástrojem gofmt | https://github.com/tisnik/wccode/blob/master/04_before_transform.go |
5 | 05_after_transform.go | zdrojový kód po transformaci nástrojem gofmt | https://github.com/tisnik/wccode/blob/master/05_after_transform.go |
6 | 06_integer_signed_types_checks.go | kontrola celočíselných konstant překladačem | https://github.com/tisnik/wccode/blob/master/06_integer_signed_types_checks.go |
7 | 07_improper_conversion.go | kontrola prováděná při typových konverzích (celočíselné datové typy) | https://github.com/tisnik/wccode/blob/master/07_improper_conversion.go |
8 | 08_fp_types_checks.go | kontrola prováděná při typových konverzích (typy s plovoucí řádovou čárkou) | https://github.com/tisnik/wccode/blob/master/08_fp_types_checks.go |
9 | 09_nil_map.go | pokus o zápis do takzvané nulové mapy (nil map) | https://github.com/tisnik/wccode/blob/master/09_nil_map.go |
10 | 10_nil_pointer.go | pokus o přístup do struktury přes nulový ukazatel (nil pointer) | https://github.com/tisnik/wccode/blob/master/10_nil_pointer.go |
11 | 11_unreachable_code.go | zdrojový kód, jehož části nejsou dosažitelné | https://github.com/tisnik/wccode/blob/master/11_unreachable_code.go |
12 | 12_shift.go | použití bitového posunu o 70 bitů v 64bitové proměnné | https://github.com/tisnik/wccode/blob/master/12_shift.go |
13 | 13_printf_checks.go | kontrola parametrů funkce fmt.Printf | https://github.com/tisnik/wccode/blob/master/13_printf_checks.go |
14 | 14_sprintf_checks.go | kontrola parametrů funkce fmt.Sprintf i její návratové hodnoty | https://github.com/tisnik/wccode/blob/master/14_sprintf_checks.go |
15 | 15_read_byte_methods.go | kontrola signatury metody ze známého rozhraní | https://github.com/tisnik/wccode/blob/master/15_read_byte_methods.go |
16 | 16_simple_server.go | jednoduchý HTTP server, ne všechny chybové kódy jsou ošetřeny | https://github.com/tisnik/wccode/blob/master/16_simple_server.go |
17 | 17_png_output.go | zápis do PNG, opět ne všechny chybové kódy jsou ošetřeny | https://github.com/tisnik/wccode/blob/master/17_png_output.go |
18 | 18_cyclomatic_complexity.go | kód pro měření cyklomatické složitosti | https://github.com/tisnik/wccode/blob/master/18_cyclomatic_complexity.go |
19 | 19_cyclomatic_complexity.go | kód pro měření cyklomatické složitosti | https://github.com/tisnik/wccode/blob/master/19_cyclomatic_complexity.go |
20 | gosec_issues1.go | zdrojový kód s několika problémy detekovatelnými nástrojem gosec | https://github.com/tisnik/wccode/blob/master/gosec_issues1.go |
21 | gosec_issues2.go | zdrojový kód s několika problémy detekovatelnými nástrojem gosec | https://github.com/tisnik/wccode/blob/master/gosec_issues2.go |
22 | gosec_issues1_nosec.go | použití poznámky #nosec pro různé bloky kódu | https://github.com/tisnik/wccode/blob/master/gosec_issues1_nosec.go |
23 | gosec_issues2_nosec.go | použití poznámky #nosec pro různé bloky kódu | https://github.com/tisnik/wccode/blob/master/gosec_issues2_nosec.go |
24 | gocritic_issues1.go | zdrojový kód s několika problémy detekovatelnými nástrojem go-critic | https://github.com/tisnik/wccode/blob/master/gocritic_issues1.go |
25 | gocritic_issues2.go | zdrojový kód s několika problémy detekovatelnými nástrojem go-critic | https://github.com/tisnik/wccode/blob/master/gocritic_issues2.go |
26 | gocritic_issues3.go | zdrojový kód s několika problémy detekovatelnými nástrojem go-critic | https://github.com/tisnik/wccode/blob/master/gocritic_issues3.go |
27 | gocritic_gosec_issues.go | zdrojový kód s několika problémy detekovatelnými nástroji gosec i go-critic | https://github.com/tisnik/wccode/blob/master/gocritic_gosec_issues.go |
28 | stat.py | statistika potenciálních problémů nalezených ve zdrojových kódech knihoven pro jazyk Go | https://github.com/tisnik/wccode/blob/master/stat.py |
20. Odkazy na Internetu
- Popis příkazu gofmt
https://pkg.go.dev/cmd/gofmt - Popis příkazu govet
https://pkg.go.dev/cmd/vet - Repositář nástroje errcheck
https://github.com/kisielk/errcheck - Repositář nástroje goconst
https://github.com/jgautheron/goconst - Repositář nástroje gocyclo
https://github.com/fzipp/gocyclo - Repositář nástroje ineffassign
https://github.com/gordonklaus/ineffassign - Repositář nástroje gosec
https://github.com/securego/gosec - Repositář nástroje go-critic
https://github.com/go-critic/go-critic - Seznam testů prováděných nástrojem go-critic
https://go-critic.com/overview - Welcome go-critic
https://itnext.io/welcome-go-critic-a26b6e30f1c6 - Don't defer Close() on writable files
https://www.joeshaw.org/dont-defer-close-on-writable-files/ - 5 Gotchas of Defer in Go — Part I
https://blog.learngoprogramming.com/gotchas-of-defer-in-go-1–8d070894cb01 - Golang Guide: A List of Top Golang Frameworks, IDEs & Tools
https://blog.intelligentbee.com/2017/08/14/golang-guide-list-top-golang-frameworks-ides-tools/ - What is the point of slice type in Go?
https://stackoverflow.com/questions/2098874/what-is-the-point-of-slice-type-in-go - The curious case of Golang array and slices
https://medium.com/@hackintoshrao/the-curious-case-of-golang-array-and-slices-2565491d4335 - Introduction to Slices in Golang
https://www.callicoder.com/golang-slices/ - Golang: Understanding ‚null‘ and nil
https://newfivefour.com/golang-null-nil.html - What does nil mean in golang?
https://stackoverflow.com/questions/35983118/what-does-nil-mean-in-golang - nils In Go
https://go101.org/article/nil.html - Go slices are not dynamic arrays
https://appliedgo.net/slices/ - The zero value of a slice is not nil
https://stackoverflow.com/questions/30806931/the-zero-value-of-a-slice-is-not-nil - Go-tcha: When nil != nil
https://dev.to/pauljlucas/go-tcha-when-nil–nil-hic - Nils in Go
https://www.doxsey.net/blog/nils-in-go