diff --git a/call_list.go b/call_list.go index 909ae32..b0e9855 100644 --- a/call_list.go +++ b/call_list.go @@ -60,6 +60,7 @@ func (c CallList) ContainsCallExpr(n ast.Node, ctx *Context) *ast.CallExpr { if err != nil { return nil } + // Try direct resolution if c.Contains(selector, ident) { return n.(*ast.CallExpr) diff --git a/rules/blacklist.go b/rules/blacklist.go index d6699ae..fbcfff0 100644 --- a/rules/blacklist.go +++ b/rules/blacklist.go @@ -16,6 +16,7 @@ package rules import ( "go/ast" + "strings" "github.com/GoASTScanner/gas" ) @@ -25,11 +26,16 @@ type blacklistedImport struct { Blacklisted map[string]string } -func (r *blacklistedImport) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err error) { +func unquote(original string) string { + copy := strings.TrimSpace(original) + copy = strings.TrimLeft(copy, `"`) + return strings.TrimRight(copy, `"`) +} + +func (r *blacklistedImport) Match(n ast.Node, c *gas.Context) (*gas.Issue, error) { if node, ok := n.(*ast.ImportSpec); ok { - description, ok := r.Blacklisted[node.Path.Value] - if ok && node.Name.String() != "_" { - return gas.NewIssue(c, n, description, r.Severity, r.Confidence), nil + if description, ok := r.Blacklisted[unquote(node.Path.Value)]; ok { + return gas.NewIssue(c, node, description, r.Severity, r.Confidence), nil } } return nil, nil @@ -50,27 +56,27 @@ func NewBlacklistedImports(conf gas.Config, blacklist map[string]string) (gas.Ru // NewBlacklistedImportMD5 fails if MD5 is imported func NewBlacklistedImportMD5(conf gas.Config) (gas.Rule, []ast.Node) { return NewBlacklistedImports(conf, map[string]string{ - "crypto/md5": "Use of weak cryptographic primitive", + "crypto/md5": "Blacklisted import crypto/md5: weak cryptographic primitive", }) } // NewBlacklistedImportDES fails if DES is imported func NewBlacklistedImportDES(conf gas.Config) (gas.Rule, []ast.Node) { return NewBlacklistedImports(conf, map[string]string{ - "crypto/des": "Use of weak cryptographic primitive", + "crypto/des": "Blacklisted import crypto/des: weak cryptographic primitive", }) } // NewBlacklistedImportRC4 fails if DES is imported func NewBlacklistedImportRC4(conf gas.Config) (gas.Rule, []ast.Node) { return NewBlacklistedImports(conf, map[string]string{ - "crypto/rc4": "Use of weak cryptographic primitive", + "crypto/rc4": "Blacklisted import crypto/rc4: weak cryptographic primitive", }) } // NewBlacklistedImportCGI fails if CGI is imported func NewBlacklistedImportCGI(conf gas.Config) (gas.Rule, []ast.Node) { return NewBlacklistedImports(conf, map[string]string{ - "net/http/cgi": "Go versions < 1.6.3 are vulnerable to Httpoxy attack: (CVE-2016-5386)", + "net/http/cgi": "Blacklisted import net/http/cgi: Go versions < 1.6.3 are vulnerable to Httpoxy attack: (CVE-2016-5386)", }) } diff --git a/rules/rules_test.go b/rules/rules_test.go index e0fd6ca..6bc0d7d 100644 --- a/rules/rules_test.go +++ b/rules/rules_test.go @@ -47,22 +47,86 @@ var _ = Describe("gas rules", func() { }) Context("report correct errors for all samples", func() { - It("should work for G101 samples", func() { + It("should detect hardcoded credentials", func() { runner("G101", testutils.SampleCodeG101) }) - It("should work for G102 samples", func() { + It("should detect binding to all network interfaces", func() { runner("G102", testutils.SampleCodeG102) }) - It("should work for G103 samples", func() { + It("should use of unsafe block", func() { runner("G103", testutils.SampleCodeG103) }) - It("should work for G104 samples", func() { + It("should errors not being checked", func() { runner("G104", testutils.SampleCodeG104) }) + It("should detect of big.Exp function", func() { + runner("G105", testutils.SampleCodeG105) + }) + + It("should detect sql injection via format strings", func() { + runner("G201", testutils.SampleCodeG201) + }) + + It("should detect sql injection via string concatenation", func() { + runner("G202", testutils.SampleCodeG202) + }) + + It("should detect unescaped html in templates", func() { + runner("G203", testutils.SampleCodeG203) + }) + + It("should detect command execution", func() { + runner("G204", testutils.SampleCodeG204) + }) + + It("should detect poor file permissions on mkdir", func() { + runner("G301", testutils.SampleCodeG301) + }) + + It("should detect poor permissions when creating or chmod a file", func() { + runner("G302", testutils.SampleCodeG302) + }) + + It("should detect insecure temp file creation", func() { + runner("G303", testutils.SampleCodeG303) + }) + + It("should detect weak crypto algorithms", func() { + runner("G401", testutils.SampleCodeG401) + }) + + It("should find insecure tls settings", func() { + runner("G402", testutils.SampleCodeG402) + }) + + It("should detect weak creation of weak rsa keys", func() { + runner("G403", testutils.SampleCodeG403) + }) + + It("should find non cryptographically secure random number sources", func() { + runner("G404", testutils.SampleCodeG404) + }) + + It("should detect blacklisted imports - MD5", func() { + runner("G501", testutils.SampleCodeG501) + }) + + It("should detect blacklisted imports - DES", func() { + runner("G502", testutils.SampleCodeG502) + }) + + It("should detect blacklisted imports - RC4", func() { + runner("G503", testutils.SampleCodeG503) + }) + + It("should detect blacklisted imports - CGI (httpoxy)", func() { + runner("G504", testutils.SampleCodeG504) + }) + }) }) diff --git a/rules/templates.go b/rules/templates.go index 78bbd92..eae3503 100644 --- a/rules/templates.go +++ b/rules/templates.go @@ -44,8 +44,9 @@ func NewTemplateCheck(conf gas.Config) (gas.Rule, []ast.Node) { calls.Add("template", "HTML") calls.Add("template", "HTMLAttr") calls.Add("template", "JS") + calls.Add("template", "URL") return &templateCheck{ - calls: gas.NewCallList(), + calls: calls, MetaData: gas.MetaData{ Severity: gas.Medium, Confidence: gas.Low, diff --git a/rules/tls.go b/rules/tls.go index 00a13f5..c0971b2 100644 --- a/rules/tls.go +++ b/rules/tls.go @@ -38,23 +38,13 @@ func stringInSlice(a string, list []string) bool { } func (t *insecureConfigTLS) processTLSCipherSuites(n ast.Node, c *gas.Context) *gas.Issue { - tlsConfig := gas.MatchCompLit(n, c, t.requiredType) - if tlsConfig == nil { - return nil - } - for _, expr := range tlsConfig.Elts { - if keyvalExpr, ok := expr.(*ast.KeyValueExpr); ok { - if keyname, ok := keyvalExpr.Key.(*ast.Ident); ok && keyname.Name == "CipherSuites" { - if ciphers, ok := keyvalExpr.Value.(*ast.CompositeLit); ok { - for _, cipher := range ciphers.Elts { - if ident, ok := cipher.(*ast.SelectorExpr); ok { - if !stringInSlice(ident.Sel.Name, t.goodCiphers) { - str := fmt.Sprintf("TLS Bad Cipher Suite: %s", ident.Sel.Name) - return gas.NewIssue(c, n, str, gas.High, gas.High) - } - } - } + if ciphers, ok := n.(*ast.CompositeLit); ok { + for _, cipher := range ciphers.Elts { + if ident, ok := cipher.(*ast.SelectorExpr); ok { + if !stringInSlice(ident.Sel.Name, t.goodCiphers) { + err := fmt.Sprintf("TLS Bad Cipher Suite: %s", ident.Sel.Name) + return gas.NewIssue(c, ident, err, gas.High, gas.High) } } } @@ -65,6 +55,7 @@ func (t *insecureConfigTLS) processTLSCipherSuites(n ast.Node, c *gas.Context) * func (t *insecureConfigTLS) processTLSConfVal(n *ast.KeyValueExpr, c *gas.Context) *gas.Issue { if ident, ok := n.Key.(*ast.Ident); ok { switch ident.Name { + case "InsecureSkipVerify": if node, ok := n.Value.(*ast.Ident); ok { if node.Name != "false" { @@ -104,7 +95,7 @@ func (t *insecureConfigTLS) processTLSConfVal(n *ast.KeyValueExpr, c *gas.Contex } case "CipherSuites": - if ret := t.processTLSCipherSuites(n, c); ret != nil { + if ret := t.processTLSCipherSuites(n.Value, c); ret != nil { return ret } @@ -114,24 +105,24 @@ func (t *insecureConfigTLS) processTLSConfVal(n *ast.KeyValueExpr, c *gas.Contex return nil } -func (t *insecureConfigTLS) Match(n ast.Node, c *gas.Context) (gi *gas.Issue, err error) { - if node := gas.MatchCompLit(n, c, t.requiredType); node != nil { - for _, elt := range node.Elts { +func (t *insecureConfigTLS) Match(n ast.Node, c *gas.Context) (*gas.Issue, error) { + if complit, ok := n.(*ast.CompositeLit); ok && c.Info.TypeOf(complit.Type).String() == t.requiredType { + for _, elt := range complit.Elts { if kve, ok := elt.(*ast.KeyValueExpr); ok { - gi = t.processTLSConfVal(kve, c) - if gi != nil { - break + issue := t.processTLSConfVal(kve, c) + if issue != nil { + return issue, nil } } } } - return + return nil, nil } // NewModernTLSCheck see: https://wiki.mozilla.org/Security/Server_Side_TLS#Modern_compatibility func NewModernTLSCheck(conf gas.Config) (gas.Rule, []ast.Node) { return &insecureConfigTLS{ - requiredType: "tls.Config", + requiredType: "crypto/tls.Config", MinVersion: 0x0303, // TLS 1.2 only MaxVersion: 0x0303, goodCiphers: []string{ @@ -146,7 +137,7 @@ func NewModernTLSCheck(conf gas.Config) (gas.Rule, []ast.Node) { // NewIntermediateTLSCheck see: https://wiki.mozilla.org/Security/Server_Side_TLS#Intermediate_compatibility_.28default.29 func NewIntermediateTLSCheck(conf gas.Config) (gas.Rule, []ast.Node) { return &insecureConfigTLS{ - requiredType: "tls.Config", + requiredType: "crypto/tls.Config", MinVersion: 0x0301, // TLS 1.2, 1.1, 1.0 MaxVersion: 0x0303, goodCiphers: []string{ @@ -172,7 +163,7 @@ func NewIntermediateTLSCheck(conf gas.Config) (gas.Rule, []ast.Node) { // NewCompatTLSCheck see: https://wiki.mozilla.org/Security/Server_Side_TLS#Old_compatibility_.28default.29 func NewCompatTLSCheck(conf gas.Config) (gas.Rule, []ast.Node) { return &insecureConfigTLS{ - requiredType: "tls.Config", + requiredType: "crypto/tls.Config", MinVersion: 0x0301, // TLS 1.2, 1.1, 1.0 MaxVersion: 0x0303, goodCiphers: []string{ diff --git a/testutils/source.go b/testutils/source.go index 91a0837..9ba1b04 100644 --- a/testutils/source.go +++ b/testutils/source.go @@ -166,6 +166,269 @@ func main() { fmt.Println(e) }`, 0}} + // SampleCodeG105 - bignum overflow + SampleCodeG105 = []CodeSample{{` +package main +import ( + "math/big" +) +func main() { + z := new(big.Int) + x := new(big.Int) + x = x.SetUint64(2) + y := new(big.Int) + y = y.SetUint64(4) + m := new(big.Int) + m = m.SetUint64(0) + z = z.Exp(x, y, m) +}`, 1}} + + // SampleCodeG201 - SQL injection via format string + SampleCodeG201 = []CodeSample{ + {` +// Format string without proper quoting +package main +import ( + "database/sql" + "fmt" + "os" + //_ "github.com/mattn/go-sqlite3" +) + +func main(){ + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + panic(err) + } + q := fmt.Sprintf("SELECT * FROM foo where name = '%s'", os.Args[1]) + rows, err := db.Query(q) + if err != nil { + panic(err) + } + defer rows.Close() +}`, 1}, { + ` +// Format string false positive +package main +import ( + "database/sql" + //_ "github.com/mattn/go-sqlite3" +) +var staticQuery = "SELECT * FROM foo WHERE age < 32" +func main(){ + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + panic(err) + } + rows, err := db.Query(staticQuery) + if err != nil { + panic(err) + } + defer rows.Close() +}`, 0}} + + // SampleCodeG202 - SQL query string building via string concatenation + SampleCodeG202 = []CodeSample{ + {` +package main +import ( + "database/sql" + //_ "github.com/mattn/go-sqlite3" + "os" +) +func main(){ + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + panic(err) + } + rows, err := db.Query("SELECT * FROM foo WHERE name = " + os.Args[1]) + if err != nil { + panic(err) + } + defer rows.Close() +}`, 1}, {` +// false positive +package main +import ( + "database/sql" + //_ "github.com/mattn/go-sqlite3" +) +var staticQuery = "SELECT * FROM foo WHERE age < " +func main(){ + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + panic(err) + } + rows, err := db.Query(staticQuery + "32") + if err != nil { + panic(err) + } + defer rows.Close() +}`, 0}, {` +package main +import ( + "database/sql" + //_ "github.com/mattn/go-sqlite3" +) +const age = "32" +var staticQuery = "SELECT * FROM foo WHERE age < " +func main(){ + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + panic(err) + } + rows, err := db.Query(staticQuery + age) + if err != nil { + panic(err) + } + defer rows.Close() +} +`, 0}} + + // SampleCodeG203 - Template checks + SampleCodeG203 = []CodeSample{ + {` +// We assume that hardcoded template strings are safe as the programmer would +// need to be explicitly shooting themselves in the foot (as below) +package main +import ( + "html/template" + "os" +) +const tmpl = "" +func main() { + t := template.Must(template.New("ex").Parse(tmpl)) + v := map[string]interface{}{ + "Title": "Test World", + "Body": template.HTML(""), + } + t.Execute(os.Stdout, v) +}`, 0}, { + ` +// Using a variable to initialize could potentially be dangerous. Under the +// current model this will likely produce some false positives. +package main +import ( + "html/template" + "os" +) +const tmpl = "" +func main() { + a := "something from another place" + t := template.Must(template.New("ex").Parse(tmpl)) + v := map[string]interface{}{ + "Title": "Test World", + "Body": template.HTML(a), + } + t.Execute(os.Stdout, v) +}`, 1}, { + ` +package main +import ( + "html/template" + "os" +) +const tmpl = "" +func main() { + a := "something from another place" + t := template.Must(template.New("ex").Parse(tmpl)) + v := map[string]interface{}{ + "Title": "Test World", + "Body": template.JS(a), + } + t.Execute(os.Stdout, v) +}`, 1}, { + ` +package main +import ( + "html/template" + "os" +) +const tmpl = "" +func main() { + a := "something from another place" + t := template.Must(template.New("ex").Parse(tmpl)) + v := map[string]interface{}{ + "Title": "Test World", + "Body": template.URL(a), + } + t.Execute(os.Stdout, v) +}`, 1}} + + // SampleCodeG204 - Subprocess auditing + SampleCodeG204 = []CodeSample{{` +package main +import "syscall" +func main() { + syscall.Exec("/bin/cat", []string{ "/etc/passwd" }, nil) +}`, 1}, {` +package main +import ( + "log" + "os/exec" +) +func main() { + cmd := exec.Command("sleep", "5") + err := cmd.Start() + if err != nil { + log.Fatal(err) + } + log.Printf("Waiting for command to finish...") + err = cmd.Wait() + log.Printf("Command finished with error: %v", err) +}`, 1}, {` +package main +import ( + "log" + "os" + "os/exec" +) +func main() { + run := "sleep" + os.Getenv("SOMETHING") + cmd := exec.Command(run, "5") + err := cmd.Start() + if err != nil { + log.Fatal(err) + } + log.Printf("Waiting for command to finish...") + err = cmd.Wait() + log.Printf("Command finished with error: %v", err) +}`, 1}} + + // SampleCodeG301 - mkdir permission check + SampleCodeG301 = []CodeSample{{` +package main +import "os" +func main() { + os.Mkdir("/tmp/mydir", 0777) + os.Mkdir("/tmp/mydir", 0600) + os.MkdirAll("/tmp/mydir/mysubidr", 0775) +}`, 2}} + + // SampleCodeG302 - file create / chmod permissions check + SampleCodeG302 = []CodeSample{{` +package main +import "os" +func main() { + os.Chmod("/tmp/somefile", 0777) + os.Chmod("/tmp/someotherfile", 0600) + os.OpenFile("/tmp/thing", os.O_CREATE|os.O_WRONLY, 0666) + os.OpenFile("/tmp/thing", os.O_CREATE|os.O_WRONLY, 0600) +}`, 2}} + + // SampleCodeG303 - bad tempfile permissions & hardcoded shared path + SampleCodeG303 = []CodeSample{{` +package samples +import ( + "io/ioutil" + "os" +) +func main() { + file1, _ := os.Create("/tmp/demo1") + defer file1.Close() + ioutil.WriteFile("/tmp/demo2", []byte("This is some data"), 0644) +}`, 2}} + // SampleCodeG401 - Use of weak crypto MD5 SampleCodeG401 = []CodeSample{ {` @@ -190,4 +453,200 @@ func main() { } fmt.Printf("%x", h.Sum(nil)) }`, 1}} + + // SampleCodeG402 - TLS settings + SampleCodeG402 = []CodeSample{{` +// InsecureSkipVerify +package main +import ( + "crypto/tls" + "fmt" + "net/http" +) +func main() { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + + client := &http.Client{Transport: tr} + _, err := client.Get("https://golang.org/") + if err != nil { + fmt.Println(err) + } +}`, 1}, { + ` +// Insecure minimum version +package main +import ( + "crypto/tls" + "fmt" + "net/http" +) +func main() { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{MinVersion: 0}, + } + client := &http.Client{Transport: tr} + _, err := client.Get("https://golang.org/") + if err != nil { + fmt.Println(err) + } +}`, 1}, {` +// Insecure max version +package main +import ( + "crypto/tls" + "fmt" + "net/http" +) +func main() { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{MaxVersion: 0}, + } + client := &http.Client{Transport: tr} + _, err := client.Get("https://golang.org/") + if err != nil { + fmt.Println(err) + } +} +`, 1}, { + ` +// Insecure ciphersuite selection +package main +import ( + "crypto/tls" + "fmt" + "net/http" +) +func main() { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{CipherSuites: []uint16{ + tls.TLS_RSA_WITH_RC4_128_SHA, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + },}, + } + client := &http.Client{Transport: tr} + _, err := client.Get("https://golang.org/") + if err != nil { + fmt.Println(err) + } +}`, 1}} + + // SampleCodeG403 - weak key strength + SampleCodeG403 = []CodeSample{ + {` +package main +import ( + "crypto/rand" + "crypto/rsa" + "fmt" +) +func main() { + //Generate Private Key + pvk, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + fmt.Println(err) + } + fmt.Println(pvk) +}`, 1}} + + // SampleCodeG404 - weak random number + SampleCodeG404 = []CodeSample{ + {` +package main +import "crypto/rand" +func main() { + good, _ := rand.Read(nil) + println(good) +}`, 0}, {` +package main +import "math/rand" +func main() { + bad := rand.Int() + println(bad) +}`, 1}, {` +package main +import ( + "crypto/rand" + mrand "math/rand" +) +func main() { + good, _ := rand.Read(nil) + println(good) + i := mrand.Int31() + println(i) +}`, 0}} + + // SampleCode501 - Blacklisted import MD5 + SampleCodeG501 = []CodeSample{ + {` +package main +import ( + "crypto/md5" + "fmt" + "os" +) +func main() { + for _, arg := range os.Args { + fmt.Printf("%x - %s\n", md5.Sum([]byte(arg)), arg) + } +}`, 1}} + + // SampleCode502 - Blacklisted import DES + SampleCodeG502 = []CodeSample{ + {` +package main +import ( + "crypto/cipher" + "crypto/des" + "crypto/rand" + "encoding/hex" + "fmt" + "io" +) +func main() { + block, err := des.NewCipher([]byte("sekritz")) + if err != nil { + panic(err) + } + plaintext := []byte("I CAN HAZ SEKRIT MSG PLZ") + ciphertext := make([]byte, des.BlockSize+len(plaintext)) + iv := ciphertext[:des.BlockSize] + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + panic(err) + } + stream := cipher.NewCFBEncrypter(block, iv) + stream.XORKeyStream(ciphertext[des.BlockSize:], plaintext) + fmt.Println("Secret message is: %s", hex.EncodeToString(ciphertext)) +}`, 1}} + + // SampleCodeG503 - Blacklisted import RC4 + SampleCodeG503 = []CodeSample{{` +package main +import ( + "crypto/rc4" + "encoding/hex" + "fmt" +) +func main() { + cipher, err := rc4.NewCipher([]byte("sekritz")) + if err != nil { + panic(err) + } + plaintext := []byte("I CAN HAZ SEKRIT MSG PLZ") + ciphertext := make([]byte, len(plaintext)) + cipher.XORKeyStream(ciphertext, plaintext) + fmt.Println("Secret message is: %s", hex.EncodeToString(ciphertext)) +}`, 1}} + + // SampleCodeG504 - Blacklisted import CGI + SampleCodeG504 = []CodeSample{{` +package main +import ( + "net/http/cgi" + "net/http" + ) +func main() { + cgi.Serve(http.FileServer(http.Dir("/usr/share/doc"))) +}`, 1}} )