Skip to content

Commit ee3146e

Browse files
caccavaleCosmin Cojocar
authored andcommitted
Rule which detects aliasing of values in RangeStmt
1 parent 8662624 commit ee3146e

File tree

5 files changed

+161
-0
lines changed

5 files changed

+161
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ directory you can supply `./...` as the input argument.
119119
- G503: Import blacklist: crypto/rc4
120120
- G504: Import blacklist: net/http/cgi
121121
- G505: Import blacklist: crypto/sha1
122+
- G601: Implicit memory aliasing of items from a range statement
122123

123124
### Retired rules
124125

rules/implicit_aliasing.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package rules
2+
3+
import (
4+
"fmt"
5+
"github.com/securego/gosec/v2"
6+
"go/ast"
7+
"go/token"
8+
)
9+
10+
type implicitAliasing struct {
11+
gosec.MetaData
12+
aliases map[*ast.Object]struct{}
13+
rightBrace token.Pos
14+
acceptableAlias []*ast.UnaryExpr
15+
}
16+
17+
func (r *implicitAliasing) ID() string {
18+
return r.MetaData.ID
19+
}
20+
21+
func containsUnary(exprs []*ast.UnaryExpr, expr *ast.UnaryExpr) bool {
22+
for _, e := range exprs {
23+
if e == expr {
24+
return true
25+
}
26+
}
27+
return false
28+
}
29+
30+
func (r *implicitAliasing) Match(n ast.Node, c *gosec.Context) (*gosec.Issue, error) {
31+
switch node := n.(type) {
32+
case *ast.RangeStmt:
33+
// When presented with a range statement, get the underlying Object bound to
34+
// by assignment and add it to our set (r.aliases) of objects to check for.
35+
if key, ok := node.Value.(*ast.Ident); ok {
36+
if assignment, ok := key.Obj.Decl.(*ast.AssignStmt); ok {
37+
if len(assignment.Lhs) < 2 {
38+
return nil, nil
39+
}
40+
41+
if object, ok := assignment.Lhs[1].(*ast.Ident); ok {
42+
r.aliases[object.Obj] = struct{}{}
43+
44+
if r.rightBrace < node.Body.Rbrace {
45+
r.rightBrace = node.Body.Rbrace
46+
}
47+
}
48+
}
49+
}
50+
case *ast.UnaryExpr:
51+
// If this unary expression is outside of the last range statement we were looking at
52+
// then clear the list of objects we're concerned about because they're no longer in
53+
// scope
54+
if node.Pos() > r.rightBrace {
55+
r.aliases = make(map[*ast.Object]struct{})
56+
r.acceptableAlias = make([]*ast.UnaryExpr, 0)
57+
}
58+
59+
// Short circuit logic to skip checking aliases if we have nothing to check against.
60+
if len(r.aliases) == 0 {
61+
return nil, nil
62+
}
63+
64+
// If this unary is at the top level of a return statement then it is okay--
65+
// see *ast.ReturnStmt comment below.
66+
if containsUnary(r.acceptableAlias, node) {
67+
return nil, nil
68+
}
69+
70+
// If we find a unary op of & (reference) of an object within r.aliases, complain.
71+
if ident, ok := node.X.(*ast.Ident); ok && node.Op.String() == "&" {
72+
if _, contains := r.aliases[ident.Obj]; contains {
73+
return gosec.NewIssue(c, n, r.ID(), r.What, r.Severity, r.Confidence), nil
74+
}
75+
}
76+
case *ast.ReturnStmt:
77+
// Returning a rangeStmt yielded value is acceptable since only one value will be returned
78+
for _, item := range node.Results {
79+
if unary, ok := item.(*ast.UnaryExpr); ok && unary.Op.String() == "&" {
80+
r.acceptableAlias = append(r.acceptableAlias, unary)
81+
}
82+
}
83+
}
84+
85+
return nil, nil
86+
}
87+
88+
// NewImplicitAliasing detects implicit memory aliasing of type: for blah := SomeCall() {... SomeOtherCall(&blah) ...}
89+
func NewImplicitAliasing(id string, conf gosec.Config) (gosec.Rule, []ast.Node) {
90+
return &implicitAliasing{
91+
aliases: make(map[*ast.Object]struct{}),
92+
rightBrace: token.NoPos,
93+
acceptableAlias: make([]*ast.UnaryExpr, 0),
94+
MetaData: gosec.MetaData{
95+
ID: id,
96+
Severity: gosec.Medium,
97+
Confidence: gosec.Medium,
98+
What: fmt.Sprintf("Implicit memory aliasing in for loop."),
99+
},
100+
}, []ast.Node{(*ast.RangeStmt)(nil), (*ast.UnaryExpr)(nil), (*ast.ReturnStmt)(nil)}
101+
}
102+
103+
/*
104+
This rule is prone to flag false positives.
105+
106+
Within GoSec, the rule is just an AST match-- there are a handful of other
107+
implementation strategies which might lend more nuance to the rule at the
108+
cost of allowing false negatives.
109+
110+
From a tooling side, I'd rather have this rule flag false positives than
111+
potentially have some false negatives-- especially if the sentiment of this
112+
rule (as I understand it, and Go) is that referencing a rangeStmt-yielded
113+
value is kinda strange and does not have a strongly justified use case.
114+
115+
Which is to say-- a false positive _should_ just be changed.
116+
*/

rules/rulelist.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ func Generate(filters ...RuleFilter) RuleList {
9696
{"G503", "Import blacklist: crypto/rc4", NewBlacklistedImportRC4},
9797
{"G504", "Import blacklist: net/http/cgi", NewBlacklistedImportCGI},
9898
{"G505", "Import blacklist: crypto/sha1", NewBlacklistedImportSHA1},
99+
100+
// memory safety
101+
{"G601", "Implicit memory aliasing in RangeStmt", NewImplicitAliasing},
99102
}
100103

101104
ruleMap := make(map[string]RuleDefinition)

rules/rules_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,10 +170,15 @@ var _ = Describe("gosec rules", func() {
170170
It("should detect blacklisted imports - CGI (httpoxy)", func() {
171171
runner("G504", testutils.SampleCodeG504)
172172
})
173+
173174
It("should detect blacklisted imports - SHA1", func() {
174175
runner("G505", testutils.SampleCodeG505)
175176
})
176177

178+
It("should detect implicit aliasing in ForRange", func() {
179+
runner("G601", testutils.SampleCodeG601)
180+
})
181+
177182
})
178183

179184
})

testutils/source.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1848,6 +1848,42 @@ func main() {
18481848
fmt.Printf("%x - %s\n", sha1.Sum([]byte(arg)), arg)
18491849
}
18501850
}`}, 1, gosec.NewConfig()}}
1851+
1852+
// SampleCodeG601 - Implicit ForRange aliasing
1853+
SampleCodeG601 = []CodeSample{{[]string{`package main
1854+
1855+
import "fmt"
1856+
1857+
var vector []*string
1858+
1859+
func appendVector(s *string) {
1860+
vector = append(vector, s)
1861+
}
1862+
1863+
func printVector() {
1864+
for _, item := range vector {
1865+
fmt.Printf("%s", *item)
1866+
}
1867+
fmt.Println()
1868+
}
1869+
1870+
func foo() (int, *string, string) {
1871+
for _, item := range vector {
1872+
return 0, &item, item
1873+
}
1874+
}
1875+
1876+
func main() {
1877+
for _, item := range []string{"A", "B", "C"} {
1878+
appendVector(&item)
1879+
}
1880+
1881+
printVector()
1882+
1883+
zero, c_star, c := foo()
1884+
fmt.Printf("%d %v %s", zero, c_start, c)
1885+
}`}, 1, gosec.NewConfig()}}
1886+
18511887
// SampleCode601 - Go build tags
18521888
SampleCode601 = []CodeSample{{[]string{`
18531889
// +build tag

0 commit comments

Comments
 (0)