WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit fdaa16d

Browse files
committed
feature: add rule for trojan source
1 parent fde7515 commit fdaa16d

File tree

5 files changed

+287
-0
lines changed

5 files changed

+287
-0
lines changed

issue/issue.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ var ruleToCWE = map[string]string{
6767
"G112": "400",
6868
"G114": "676",
6969
"G115": "190",
70+
"G116": "838",
7071
"G201": "89",
7172
"G202": "89",
7273
"G203": "79",

rules/rulelist.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ func Generate(trackSuppressions bool, filters ...RuleFilter) RuleList {
7676
{"G111", "Detect http.Dir('/') as a potential risk", NewDirectoryTraversal},
7777
{"G112", "Detect ReadHeaderTimeout not configured as a potential risk", NewSlowloris},
7878
{"G114", "Use of net/http serve function that has no support for setting timeouts", NewHTTPServeWithoutTimeouts},
79+
{"G116", "Detect Trojan Source attacks using bidirectional Unicode characters", NewTrojanSource},
7980

8081
// injection
8182
{"G201", "SQL query construction using format string", NewSQLStrFormat},

rules/rules_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ var _ = Describe("gosec rules", func() {
107107
runner("G114", testutils.SampleCodeG114)
108108
})
109109

110+
It("should detect Trojan Source attacks using bidirectional Unicode characters", func() {
111+
runner("G116", testutils.SampleCodeG116)
112+
})
113+
110114
It("should detect sql injection via format strings", func() {
111115
runner("G201", testutils.SampleCodeG201)
112116
})

rules/trojansource.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package rules
2+
3+
import (
4+
"go/ast"
5+
"os"
6+
7+
"github.com/securego/gosec/v2"
8+
"github.com/securego/gosec/v2/issue"
9+
)
10+
11+
type trojanSource struct {
12+
issue.MetaData
13+
bidiChars map[rune]struct{}
14+
}
15+
16+
func (r *trojanSource) ID() string {
17+
return r.MetaData.ID
18+
}
19+
20+
func (r *trojanSource) Match(node ast.Node, c *gosec.Context) (*issue.Issue, error) {
21+
if file, ok := node.(*ast.File); ok {
22+
fobj := c.FileSet.File(file.Pos())
23+
if fobj == nil {
24+
return nil, nil
25+
}
26+
27+
content, err := os.ReadFile(fobj.Name())
28+
if err != nil {
29+
return nil, nil
30+
}
31+
32+
for _, ch := range string(content) {
33+
if _, exists := r.bidiChars[ch]; exists {
34+
return c.NewIssue(node, r.ID(), r.What, r.Severity, r.Confidence), nil
35+
}
36+
}
37+
}
38+
39+
return nil, nil
40+
}
41+
42+
func NewTrojanSource(id string, _ gosec.Config) (gosec.Rule, []ast.Node) {
43+
return &trojanSource{
44+
MetaData: issue.MetaData{
45+
ID: id,
46+
Severity: issue.High,
47+
Confidence: issue.Medium,
48+
What: "Potential Trojan Source vulnerability via use of bidirectional text control characters",
49+
},
50+
bidiChars: map[rune]struct{}{
51+
'\u202a': {},
52+
'\u202b': {},
53+
'\u202c': {},
54+
'\u202d': {},
55+
'\u202e': {},
56+
'\u2066': {},
57+
'\u2067': {},
58+
'\u2068': {},
59+
'\u2069': {},
60+
'\u200e': {},
61+
'\u200f': {},
62+
},
63+
}, []ast.Node{(*ast.File)(nil)}
64+
}

testutils/g116_samples.go

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
package testutils
2+
3+
import "github.com/securego/gosec/v2"
4+
5+
// #nosec - This file intentionally contains bidirectional Unicode characters
6+
// for testing trojan source detection. The G116 rule scans the entire file content (not just AST nodes)
7+
// because trojan source attacks work by manipulating visual representation of code through bidirectional
8+
// text control characters, which can appear in comments, strings or anywhere in the source file.
9+
// Without this #nosec exclusion, gosec would detect these test samples as actual vulnerabilities.
10+
var (
11+
// SampleCodeG116 - TrojanSource code snippets
12+
SampleCodeG116 = []CodeSample{
13+
{[]string{`
14+
package main
15+
16+
import "fmt"
17+
18+
func main() {
19+
// This comment contains bidirectional unicode: access‮⁦ granted⁩‭
20+
isAdmin := false
21+
fmt.Println("Access status:", isAdmin)
22+
}
23+
`}, 1, gosec.NewConfig()},
24+
{[]string{`
25+
package main
26+
27+
import "fmt"
28+
29+
func main() {
30+
// Trojan source with RLO character
31+
accessLevel := "user"
32+
// Actually assigns "nimda" due to bidi chars: accessLevel = "‮nimda"
33+
if accessLevel == "admin" {
34+
fmt.Println("Access granted")
35+
}
36+
}
37+
`}, 1, gosec.NewConfig()},
38+
{[]string{`
39+
package main
40+
41+
import "fmt"
42+
43+
func main() {
44+
// String with bidirectional override
45+
username := "admin‮ ⁦Check if admin⁩ ⁦"
46+
password := "secret"
47+
fmt.Println(username, password)
48+
}
49+
`}, 1, gosec.NewConfig()},
50+
{[]string{`
51+
package main
52+
53+
import "fmt"
54+
55+
func main() {
56+
// Contains LRI (Left-to-Right Isolate) U+2066
57+
comment := "Safe comment ⁦with hidden text⁩"
58+
fmt.Println(comment)
59+
}
60+
`}, 1, gosec.NewConfig()},
61+
{[]string{`
62+
package main
63+
64+
import "fmt"
65+
66+
func main() {
67+
// Contains RLI (Right-to-Left Isolate) U+2067
68+
message := "Normal text ⁧hidden⁩"
69+
fmt.Println(message)
70+
}
71+
`}, 1, gosec.NewConfig()},
72+
{[]string{`
73+
package main
74+
75+
import "fmt"
76+
77+
func main() {
78+
// Contains FSI (First Strong Isolate) U+2068
79+
text := "Text with ⁨hidden content⁩"
80+
fmt.Println(text)
81+
}
82+
`}, 1, gosec.NewConfig()},
83+
{[]string{`
84+
package main
85+
86+
import "fmt"
87+
88+
func main() {
89+
// Contains LRE (Left-to-Right Embedding) U+202A
90+
embedded := "Text with ‪embedded‬ content"
91+
fmt.Println(embedded)
92+
}
93+
`}, 1, gosec.NewConfig()},
94+
{[]string{`
95+
package main
96+
97+
import "fmt"
98+
99+
func main() {
100+
// Contains RLE (Right-to-Left Embedding) U+202B
101+
rtlEmbedded := "Text with ‫embedded‬ content"
102+
fmt.Println(rtlEmbedded)
103+
}
104+
`}, 1, gosec.NewConfig()},
105+
{[]string{`
106+
package main
107+
108+
import "fmt"
109+
110+
func main() {
111+
// Contains PDF (Pop Directional Formatting) U+202C
112+
formatted := "Text with ‬formatting"
113+
fmt.Println(formatted)
114+
}
115+
`}, 1, gosec.NewConfig()},
116+
{[]string{`
117+
package main
118+
119+
import "fmt"
120+
121+
func main() {
122+
// Contains LRO (Left-to-Right Override) U+202D
123+
override := "Text ‭override"
124+
fmt.Println(override)
125+
}
126+
`}, 1, gosec.NewConfig()},
127+
{[]string{`
128+
package main
129+
130+
import "fmt"
131+
132+
func main() {
133+
// Contains RLO (Right-to-Left Override) U+202E
134+
rloText := "Text ‮override"
135+
fmt.Println(rloText)
136+
}
137+
`}, 1, gosec.NewConfig()},
138+
{[]string{`
139+
package main
140+
141+
import "fmt"
142+
143+
func main() {
144+
// Contains RLM (Right-to-Left Mark) U+200F
145+
marked := "Text ‏marked"
146+
fmt.Println(marked)
147+
}
148+
`}, 1, gosec.NewConfig()},
149+
{[]string{`
150+
package main
151+
152+
import "fmt"
153+
154+
func main() {
155+
// Contains LRM (Left-to-Right Mark) U+200E
156+
lrmText := "Text ‎marked"
157+
fmt.Println(lrmText)
158+
}
159+
`}, 1, gosec.NewConfig()},
160+
{[]string{`
161+
package main
162+
163+
import "fmt"
164+
165+
// Safe code without bidirectional characters
166+
func main() {
167+
username := "admin"
168+
password := "secret"
169+
fmt.Println("Username:", username)
170+
fmt.Println("Password:", password)
171+
}
172+
`}, 0, gosec.NewConfig()},
173+
{[]string{`
174+
package main
175+
176+
import "fmt"
177+
178+
// Normal comment with regular text
179+
func main() {
180+
// This is a safe comment
181+
isAdmin := true
182+
if isAdmin {
183+
fmt.Println("Access granted")
184+
}
185+
}
186+
`}, 0, gosec.NewConfig()},
187+
{[]string{`
188+
package main
189+
190+
import "fmt"
191+
192+
func main() {
193+
// Regular ASCII characters only
194+
message := "Hello, World!"
195+
fmt.Println(message)
196+
}
197+
`}, 0, gosec.NewConfig()},
198+
{[]string{`
199+
package main
200+
201+
import "fmt"
202+
203+
func authenticateUser(username, password string) bool {
204+
// Normal authentication logic
205+
if username == "admin" && password == "secret" {
206+
return true
207+
}
208+
return false
209+
}
210+
211+
func main() {
212+
result := authenticateUser("user", "pass")
213+
fmt.Println("Authenticated:", result)
214+
}
215+
`}, 0, gosec.NewConfig()},
216+
}
217+
)

0 commit comments

Comments
 (0)