Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions issue/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ var ruleToCWE = map[string]string{
"G112": "400",
"G114": "676",
"G115": "190",
"G116": "838",
"G201": "89",
"G202": "89",
"G203": "79",
Expand Down
1 change: 1 addition & 0 deletions rules/rulelist.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
4 changes: 4 additions & 0 deletions rules/rules_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
Expand Down
96 changes: 96 additions & 0 deletions rules/trojansource.go
Original file line number Diff line number Diff line change
@@ -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())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this perform well with large files?

I would use something like to make sure that we don't run out of memory and have performance issues:

file, err := os.Open("")
if err != nil {
    log.Fatal(err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
    line := scanner.Text()
    // Process line
}
if err := scanner.Err(); err != nil {
    log.Fatal(err)
}

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)}
}
217 changes: 217 additions & 0 deletions testutils/g116_samples.go
Original file line number Diff line number Diff line change
@@ -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()},
}
)