diff --git a/issue/issue.go b/issue/issue.go index b7c804e455..28c876b339 100644 --- a/issue/issue.go +++ b/issue/issue.go @@ -67,6 +67,7 @@ var ruleToCWE = map[string]string{ "G112": "400", "G114": "676", "G115": "190", + "G116": "838", "G201": "89", "G202": "89", "G203": "79", diff --git a/rules/rulelist.go b/rules/rulelist.go index 3f8598009f..bd8dbbb7dd 100644 --- a/rules/rulelist.go +++ b/rules/rulelist.go @@ -76,6 +76,7 @@ func Generate(trackSuppressions bool, filters ...RuleFilter) RuleList { {"G111", "Detect http.Dir('/') as a potential risk", NewDirectoryTraversal}, {"G112", "Detect ReadHeaderTimeout not configured as a potential risk", NewSlowloris}, {"G114", "Use of net/http serve function that has no support for setting timeouts", NewHTTPServeWithoutTimeouts}, + {"G116", "Detect Trojan Source attacks using bidirectional Unicode characters", NewTrojanSource}, // injection {"G201", "SQL query construction using format string", NewSQLStrFormat}, diff --git a/rules/rules_test.go b/rules/rules_test.go index 0f5314bc37..113ffb4bfb 100644 --- a/rules/rules_test.go +++ b/rules/rules_test.go @@ -107,6 +107,10 @@ var _ = Describe("gosec rules", func() { runner("G114", testutils.SampleCodeG114) }) + It("should detect Trojan Source attacks using bidirectional Unicode characters", func() { + runner("G116", testutils.SampleCodeG116) + }) + It("should detect sql injection via format strings", func() { runner("G201", testutils.SampleCodeG201) }) diff --git a/rules/trojansource.go b/rules/trojansource.go new file mode 100644 index 0000000000..e2765d2695 --- /dev/null +++ b/rules/trojansource.go @@ -0,0 +1,96 @@ +package rules + +import ( + "go/ast" + "os" + + "github.com/securego/gosec/v2" + "github.com/securego/gosec/v2/issue" +) + +type trojanSource struct { + issue.MetaData + bidiChars map[rune]struct{} +} + +func (r *trojanSource) ID() string { + return r.MetaData.ID +} + +func (r *trojanSource) Match(node ast.Node, c *gosec.Context) (*issue.Issue, error) { + if file, ok := node.(*ast.File); ok { + fobj := c.FileSet.File(file.Pos()) + if fobj == nil { + return nil, nil + } + + content, err := os.ReadFile(fobj.Name()) + if err != nil { + return nil, nil + } + + for _, ch := range string(content) { + if _, exists := r.bidiChars[ch]; exists { + return c.NewIssue(node, r.ID(), r.What, r.Severity, r.Confidence), nil + } + } + } + + return nil, nil +} + +// func (r *trojanSource) Match(node ast.Node, c *gosec.Context) (*issue.Issue, error) { +// if file, ok := node.(*ast.File); ok { +// fobj := c.FileSet.File(file.Pos()) +// if fobj == nil { +// return nil, nil +// } + +// file, err := os.Open(fobj.Name()) +// if err != nil { +// log.Fatal(err) +// } + +// defer file.Close() + +// scanner := bufio.NewScanner(file) +// for scanner.Scan() { +// line := scanner.Text() +// for _, ch := range line { +// if _, exists := r.bidiChars[ch]; exists { +// return c.NewIssue(node, r.ID(), r.What, r.Severity, r.Confidence), nil +// } +// } +// } + +// if err := scanner.Err(); err != nil { +// log.Fatal(err) +// } +// } + +// return nil, nil +// } + +func NewTrojanSource(id string, _ gosec.Config) (gosec.Rule, []ast.Node) { + return &trojanSource{ + MetaData: issue.MetaData{ + ID: id, + Severity: issue.High, + Confidence: issue.Medium, + What: "Potential Trojan Source vulnerability via use of bidirectional text control characters", + }, + bidiChars: map[rune]struct{}{ + '\u202a': {}, + '\u202b': {}, + '\u202c': {}, + '\u202d': {}, + '\u202e': {}, + '\u2066': {}, + '\u2067': {}, + '\u2068': {}, + '\u2069': {}, + '\u200e': {}, + '\u200f': {}, + }, + }, []ast.Node{(*ast.File)(nil)} +} diff --git a/testutils/g116_samples.go b/testutils/g116_samples.go new file mode 100644 index 0000000000..76f9fa8d5a --- /dev/null +++ b/testutils/g116_samples.go @@ -0,0 +1,217 @@ +package testutils + +import "github.com/securego/gosec/v2" + +// #nosec - This file intentionally contains bidirectional Unicode characters +// for testing trojan source detection. The G116 rule scans the entire file content (not just AST nodes) +// because trojan source attacks work by manipulating visual representation of code through bidirectional +// text control characters, which can appear in comments, strings or anywhere in the source file. +// Without this #nosec exclusion, gosec would detect these test samples as actual vulnerabilities. +var ( + // SampleCodeG116 - TrojanSource code snippets + SampleCodeG116 = []CodeSample{ + {[]string{` +package main + +import "fmt" + +func main() { + // This comment contains bidirectional unicode: access‮⁦ granted⁩‭ + isAdmin := false + fmt.Println("Access status:", isAdmin) +} +`}, 1, gosec.NewConfig()}, + {[]string{` +package main + +import "fmt" + +func main() { + // Trojan source with RLO character + accessLevel := "user" + // Actually assigns "nimda" due to bidi chars: accessLevel = "‮nimda" + if accessLevel == "admin" { + fmt.Println("Access granted") + } +} +`}, 1, gosec.NewConfig()}, + {[]string{` +package main + +import "fmt" + +func main() { + // String with bidirectional override + username := "admin‮ ⁦Check if admin⁩ ⁦" + password := "secret" + fmt.Println(username, password) +} +`}, 1, gosec.NewConfig()}, + {[]string{` +package main + +import "fmt" + +func main() { + // Contains LRI (Left-to-Right Isolate) U+2066 + comment := "Safe comment ⁦with hidden text⁩" + fmt.Println(comment) +} +`}, 1, gosec.NewConfig()}, + {[]string{` +package main + +import "fmt" + +func main() { + // Contains RLI (Right-to-Left Isolate) U+2067 + message := "Normal text ⁧hidden⁩" + fmt.Println(message) +} +`}, 1, gosec.NewConfig()}, + {[]string{` +package main + +import "fmt" + +func main() { + // Contains FSI (First Strong Isolate) U+2068 + text := "Text with ⁨hidden content⁩" + fmt.Println(text) +} +`}, 1, gosec.NewConfig()}, + {[]string{` +package main + +import "fmt" + +func main() { + // Contains LRE (Left-to-Right Embedding) U+202A + embedded := "Text with ‪embedded‬ content" + fmt.Println(embedded) +} +`}, 1, gosec.NewConfig()}, + {[]string{` +package main + +import "fmt" + +func main() { + // Contains RLE (Right-to-Left Embedding) U+202B + rtlEmbedded := "Text with ‫embedded‬ content" + fmt.Println(rtlEmbedded) +} +`}, 1, gosec.NewConfig()}, + {[]string{` +package main + +import "fmt" + +func main() { + // Contains PDF (Pop Directional Formatting) U+202C + formatted := "Text with ‬formatting" + fmt.Println(formatted) +} +`}, 1, gosec.NewConfig()}, + {[]string{` +package main + +import "fmt" + +func main() { + // Contains LRO (Left-to-Right Override) U+202D + override := "Text ‭override" + fmt.Println(override) +} +`}, 1, gosec.NewConfig()}, + {[]string{` +package main + +import "fmt" + +func main() { + // Contains RLO (Right-to-Left Override) U+202E + rloText := "Text ‮override" + fmt.Println(rloText) +} +`}, 1, gosec.NewConfig()}, + {[]string{` +package main + +import "fmt" + +func main() { + // Contains RLM (Right-to-Left Mark) U+200F + marked := "Text ‏marked" + fmt.Println(marked) +} +`}, 1, gosec.NewConfig()}, + {[]string{` +package main + +import "fmt" + +func main() { + // Contains LRM (Left-to-Right Mark) U+200E + lrmText := "Text ‎marked" + fmt.Println(lrmText) +} +`}, 1, gosec.NewConfig()}, + {[]string{` +package main + +import "fmt" + +// Safe code without bidirectional characters +func main() { + username := "admin" + password := "secret" + fmt.Println("Username:", username) + fmt.Println("Password:", password) +} +`}, 0, gosec.NewConfig()}, + {[]string{` +package main + +import "fmt" + +// Normal comment with regular text +func main() { + // This is a safe comment + isAdmin := true + if isAdmin { + fmt.Println("Access granted") + } +} +`}, 0, gosec.NewConfig()}, + {[]string{` +package main + +import "fmt" + +func main() { + // Regular ASCII characters only + message := "Hello, World!" + fmt.Println(message) +} +`}, 0, gosec.NewConfig()}, + {[]string{` +package main + +import "fmt" + +func authenticateUser(username, password string) bool { + // Normal authentication logic + if username == "admin" && password == "secret" { + return true + } + return false +} + +func main() { + result := authenticateUser("user", "pass") + fmt.Println("Authenticated:", result) +} +`}, 0, gosec.NewConfig()}, + } +)