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{`

Check failure on line 13 in testutils/g116_samples.go

View workflow job for this annotation

GitHub Actions / test (1.24.11, latest)

ST1018: string literal contains Unicode format characters, consider using escape sequences instead (staticcheck)
package main

import "fmt"

func main() {
// This comment contains bidirectional unicode: access‮⁦ granted⁩‭
isAdmin := false
fmt.Println("Access status:", isAdmin)
}
`}, 1, gosec.NewConfig()},
{[]string{`

Check failure on line 24 in testutils/g116_samples.go

View workflow job for this annotation

GitHub Actions / test (1.24.11, latest)

ST1018: string literal contains the Unicode format character U+202E, consider using the '\u202e' escape sequence instead (staticcheck)
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{`

Check failure on line 38 in testutils/g116_samples.go

View workflow job for this annotation

GitHub Actions / test (1.24.11, latest)

ST1018: string literal contains Unicode format characters, consider using escape sequences instead (staticcheck)
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{`

Check failure on line 50 in testutils/g116_samples.go

View workflow job for this annotation

GitHub Actions / test (1.24.11, latest)

ST1018: string literal contains Unicode format characters, consider using escape sequences instead (staticcheck)
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{`

Check failure on line 105 in testutils/g116_samples.go

View workflow job for this annotation

GitHub Actions / test (1.24.11, latest)

ST1018: string literal contains the Unicode format character U+202C, consider using the '\u202c' escape sequence instead (staticcheck)
package main

import "fmt"

func main() {
// Contains PDF (Pop Directional Formatting) U+202C
formatted := "Text with ‬formatting"
fmt.Println(formatted)
}
`}, 1, gosec.NewConfig()},
{[]string{`

Check failure on line 116 in testutils/g116_samples.go

View workflow job for this annotation

GitHub Actions / test (1.24.11, latest)

ST1018: string literal contains the Unicode format character U+202D, consider using the '\u202d' escape sequence instead (staticcheck)
package main

import "fmt"

func main() {
// Contains LRO (Left-to-Right Override) U+202D
override := "Text ‭override"
fmt.Println(override)
}
`}, 1, gosec.NewConfig()},
{[]string{`

Check failure on line 127 in testutils/g116_samples.go

View workflow job for this annotation

GitHub Actions / test (1.24.11, latest)

ST1018: string literal contains the Unicode format character U+202E, consider using the '\u202e' escape sequence instead (staticcheck)
package main

import "fmt"

func main() {
// Contains RLO (Right-to-Left Override) U+202E
rloText := "Text ‮override"
fmt.Println(rloText)
}
`}, 1, gosec.NewConfig()},
{[]string{`

Check failure on line 138 in testutils/g116_samples.go

View workflow job for this annotation

GitHub Actions / test (1.24.11, latest)

ST1018: string literal contains the Unicode format character U+200F, consider using the '\u200f' escape sequence instead (staticcheck)
package main

import "fmt"

func main() {
// Contains RLM (Right-to-Left Mark) U+200F
marked := "Text ‏marked"
fmt.Println(marked)
}
`}, 1, gosec.NewConfig()},
{[]string{`

Check failure on line 149 in testutils/g116_samples.go

View workflow job for this annotation

GitHub Actions / test (1.24.11, latest)

ST1018: string literal contains the Unicode format character U+200E, consider using the '\u200e' escape sequence instead (staticcheck)
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()},
}
)
Loading