Skip to content

Commit 762ff3a

Browse files
dhuiCosmin Cojocar
authored andcommitted
Allow quoted strings to be used to format SQL queries (#240)
* Support stripping vendor paths when matching calls * Factor out matching of formatter string * Quoted strings are safe to use with SQL str formatted strings * Add test for allowing quoted strings with string formatters * Install the pq package for tests to pass
1 parent ec32ce6 commit 762ff3a

File tree

14 files changed

+88
-33
lines changed

14 files changed

+88
-33
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ install:
1212
- go get -u github.com/onsi/ginkgo/ginkgo
1313
- go get -u github.com/onsi/gomega
1414
- go get -u golang.org/x/crypto/ssh
15+
- go get -u github.com/lib/pq
1516
- go get -u github.com/securego/gosec/cmd/gosec/...
1617
- go get -v -t ./...
1718
- export PATH=$PATH:$HOME/gopath/bin

call_list.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@ package gosec
1515

1616
import (
1717
"go/ast"
18+
"strings"
1819
)
1920

21+
const vendorPath = "vendor/"
22+
2023
type set map[string]bool
2124

2225
// CallList is used to check for usage of specific packages
@@ -55,17 +58,27 @@ func (c CallList) Contains(selector, ident string) bool {
5558

5659
// ContainsCallExpr resolves the call expression name and type
5760
/// or package and determines if it exists within the CallList
58-
func (c CallList) ContainsCallExpr(n ast.Node, ctx *Context) *ast.CallExpr {
61+
func (c CallList) ContainsCallExpr(n ast.Node, ctx *Context, stripVendor bool) *ast.CallExpr {
5962
selector, ident, err := GetCallInfo(n, ctx)
6063
if err != nil {
6164
return nil
6265
}
6366

64-
// Use only explicit path to reduce conflicts
65-
if path, ok := GetImportPath(selector, ctx); ok && c.Contains(path, ident) {
66-
return n.(*ast.CallExpr)
67+
// Use only explicit path (optionally strip vendor path prefix) to reduce conflicts
68+
path, ok := GetImportPath(selector, ctx)
69+
if !ok {
70+
return nil
71+
}
72+
if stripVendor {
73+
if vendorIdx := strings.Index(path, vendorPath); vendorIdx >= 0 {
74+
path = path[vendorIdx+len(vendorPath):]
75+
}
76+
}
77+
if !c.Contains(path, ident) {
78+
return nil
6779
}
6880

81+
return n.(*ast.CallExpr)
6982
/*
7083
// Try direct resolution
7184
if c.Contains(selector, ident) {
@@ -74,5 +87,4 @@ func (c CallList) ContainsCallExpr(n ast.Node, ctx *Context) *ast.CallExpr {
7487
}
7588
*/
7689

77-
return nil
7890
}

call_list_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ var _ = Describe("call list", func() {
7373
v := testutils.NewMockVisitor()
7474
v.Context = ctx
7575
v.Callback = func(n ast.Node, ctx *gosec.Context) bool {
76-
if _, ok := n.(*ast.CallExpr); ok && calls.ContainsCallExpr(n, ctx) != nil {
76+
if _, ok := n.(*ast.CallExpr); ok && calls.ContainsCallExpr(n, ctx, false) != nil {
7777
matched++
7878
}
7979
return true

rules/archive.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ func (a *archive) ID() string {
1919

2020
// Match inspects AST nodes to determine if the filepath.Joins uses any argument derived from type zip.File
2121
func (a *archive) Match(n ast.Node, c *gosec.Context) (*gosec.Issue, error) {
22-
if node := a.calls.ContainsCallExpr(n, c); node != nil {
22+
if node := a.calls.ContainsCallExpr(n, c, false); node != nil {
2323
for _, arg := range node.Args {
2424
var argType types.Type
2525
if selector, ok := arg.(*ast.SelectorExpr); ok {

rules/bind.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func (r *bindsToAllNetworkInterfaces) ID() string {
3333
}
3434

3535
func (r *bindsToAllNetworkInterfaces) Match(n ast.Node, c *gosec.Context) (*gosec.Issue, error) {
36-
callExpr := r.calls.ContainsCallExpr(n, c)
36+
callExpr := r.calls.ContainsCallExpr(n, c, false)
3737
if callExpr == nil {
3838
return nil, nil
3939
}

rules/errors.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func (r *noErrorCheck) Match(n ast.Node, ctx *gosec.Context) (*gosec.Issue, erro
5353
switch stmt := n.(type) {
5454
case *ast.AssignStmt:
5555
for _, expr := range stmt.Rhs {
56-
if callExpr, ok := expr.(*ast.CallExpr); ok && r.whitelist.ContainsCallExpr(expr, ctx) == nil {
56+
if callExpr, ok := expr.(*ast.CallExpr); ok && r.whitelist.ContainsCallExpr(expr, ctx, false) == nil {
5757
pos := returnsError(callExpr, ctx)
5858
if pos < 0 || pos >= len(stmt.Lhs) {
5959
return nil, nil
@@ -64,7 +64,7 @@ func (r *noErrorCheck) Match(n ast.Node, ctx *gosec.Context) (*gosec.Issue, erro
6464
}
6565
}
6666
case *ast.ExprStmt:
67-
if callExpr, ok := stmt.X.(*ast.CallExpr); ok && r.whitelist.ContainsCallExpr(stmt.X, ctx) == nil {
67+
if callExpr, ok := stmt.X.(*ast.CallExpr); ok && r.whitelist.ContainsCallExpr(stmt.X, ctx, false) == nil {
6868
pos := returnsError(callExpr, ctx)
6969
if pos >= 0 {
7070
return gosec.NewIssue(ctx, n, r.ID(), r.What, r.Severity, r.Confidence), nil

rules/readfile.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func (r *readfile) ID() string {
3434

3535
// isJoinFunc checks if there is a filepath.Join or other join function
3636
func (r *readfile) isJoinFunc(n ast.Node, c *gosec.Context) bool {
37-
if call := r.pathJoin.ContainsCallExpr(n, c); call != nil {
37+
if call := r.pathJoin.ContainsCallExpr(n, c, false); call != nil {
3838
for _, arg := range call.Args {
3939
// edge case: check if one of the args is a BinaryExpr
4040
if binExp, ok := arg.(*ast.BinaryExpr); ok {
@@ -44,21 +44,21 @@ func (r *readfile) isJoinFunc(n ast.Node, c *gosec.Context) bool {
4444
}
4545
}
4646

47-
// try and resolve identity
48-
if ident, ok := arg.(*ast.Ident); ok {
49-
obj := c.Info.ObjectOf(ident)
50-
if _, ok := obj.(*types.Var); ok && !gosec.TryResolve(ident, c) {
51-
return true
47+
// try and resolve identity
48+
if ident, ok := arg.(*ast.Ident); ok {
49+
obj := c.Info.ObjectOf(ident)
50+
if _, ok := obj.(*types.Var); ok && !gosec.TryResolve(ident, c) {
51+
return true
52+
}
5253
}
5354
}
5455
}
55-
}
5656
return false
5757
}
5858

5959
// Match inspects AST nodes to determine if the match the methods `os.Open` or `ioutil.ReadFile`
6060
func (r *readfile) Match(n ast.Node, c *gosec.Context) (*gosec.Issue, error) {
61-
if node := r.ContainsCallExpr(n, c); node != nil {
61+
if node := r.ContainsCallExpr(n, c, false); node != nil {
6262
for _, arg := range node.Args {
6363
// handles path joining functions in Arg
6464
// eg. os.Open(filepath.Join("/tmp/", file))

rules/rsa.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func (w *weakKeyStrength) ID() string {
3232
}
3333

3434
func (w *weakKeyStrength) Match(n ast.Node, c *gosec.Context) (*gosec.Issue, error) {
35-
if callExpr := w.calls.ContainsCallExpr(n, c); callExpr != nil {
35+
if callExpr := w.calls.ContainsCallExpr(n, c, false); callExpr != nil {
3636
if bits, err := gosec.GetInt(callExpr.Args[1]); err == nil && bits < (int64)(w.bits) {
3737
return gosec.NewIssue(c, n, w.ID(), w.What, w.Severity, w.Confidence), nil
3838
}

rules/sql.go

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,9 @@ func NewSQLStrConcat(id string, conf gosec.Config) (gosec.Rule, []ast.Node) {
9898

9999
type sqlStrFormat struct {
100100
sqlStatement
101-
calls gosec.CallList
102-
noIssue gosec.CallList
101+
calls gosec.CallList
102+
noIssue gosec.CallList
103+
noIssueQuoted gosec.CallList
103104
}
104105

105106
// Looks for "fmt.Sprintf("SELECT * FROM foo where '%s', userInput)"
@@ -109,7 +110,7 @@ func (s *sqlStrFormat) Match(n ast.Node, c *gosec.Context) (*gosec.Issue, error)
109110
argIndex := 0
110111

111112
// TODO(gm) improve confidence if database/sql is being used
112-
if node := s.calls.ContainsCallExpr(n, c); node != nil {
113+
if node := s.calls.ContainsCallExpr(n, c, false); node != nil {
113114
// if the function is fmt.Fprintf, search for SQL statement in Args[1] instead
114115
if sel, ok := node.Fun.(*ast.SelectorExpr); ok {
115116
if sel.Sel.Name == "Fprintf" {
@@ -125,17 +126,35 @@ func (s *sqlStrFormat) Match(n ast.Node, c *gosec.Context) (*gosec.Issue, error)
125126
argIndex = 1
126127
}
127128
}
129+
130+
var formatter string
131+
128132
// concats callexpr arg strings together if needed before regex evaluation
129133
if argExpr, ok := node.Args[argIndex].(*ast.BinaryExpr); ok {
130134
if fullStr, ok := gosec.ConcatString(argExpr); ok {
131-
if s.MatchPatterns(fullStr) {
132-
return gosec.NewIssue(c, n, s.ID(), s.What, s.Severity, s.Confidence),
133-
nil
134-
}
135+
formatter = fullStr
135136
}
137+
} else if arg, e := gosec.GetString(node.Args[argIndex]); e == nil {
138+
formatter = arg
139+
}
140+
if len(formatter) <= 0 {
141+
return nil, nil
136142
}
137143

138-
if arg, e := gosec.GetString(node.Args[argIndex]); s.MatchPatterns(arg) && e == nil {
144+
// If all formatter args are quoted, then the SQL construction is safe
145+
if argIndex+1 < len(node.Args) {
146+
allQuoted := true
147+
for _, arg := range node.Args[argIndex+1:] {
148+
if n := s.noIssueQuoted.ContainsCallExpr(arg, c, true); n == nil {
149+
allQuoted = false
150+
break
151+
}
152+
}
153+
if allQuoted {
154+
return nil, nil
155+
}
156+
}
157+
if s.MatchPatterns(formatter) {
139158
return gosec.NewIssue(c, n, s.ID(), s.What, s.Severity, s.Confidence), nil
140159
}
141160
}
@@ -145,8 +164,9 @@ func (s *sqlStrFormat) Match(n ast.Node, c *gosec.Context) (*gosec.Issue, error)
145164
// NewSQLStrFormat looks for cases where we're building SQL query strings using format strings
146165
func NewSQLStrFormat(id string, conf gosec.Config) (gosec.Rule, []ast.Node) {
147166
rule := &sqlStrFormat{
148-
calls: gosec.NewCallList(),
149-
noIssue: gosec.NewCallList(),
167+
calls: gosec.NewCallList(),
168+
noIssue: gosec.NewCallList(),
169+
noIssueQuoted: gosec.NewCallList(),
150170
sqlStatement: sqlStatement{
151171
patterns: []*regexp.Regexp{
152172
regexp.MustCompile("(?)(SELECT|DELETE|INSERT|UPDATE|INTO|FROM|WHERE) "),
@@ -162,5 +182,6 @@ func NewSQLStrFormat(id string, conf gosec.Config) (gosec.Rule, []ast.Node) {
162182
}
163183
rule.calls.AddAll("fmt", "Sprint", "Sprintf", "Sprintln", "Fprintf")
164184
rule.noIssue.AddAll("os", "Stdout", "Stderr")
185+
rule.noIssueQuoted.Add("github.com/lib/pq", "QuoteIdentifier")
165186
return rule, []ast.Node{(*ast.CallExpr)(nil)}
166187
}

rules/ssrf.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func (r *ssrf) ResolveVar(n *ast.CallExpr, c *gosec.Context) bool {
3535
// Match inspects AST nodes to determine if certain net/http methods are called with variable input
3636
func (r *ssrf) Match(n ast.Node, c *gosec.Context) (*gosec.Issue, error) {
3737
// Call expression is using http package directly
38-
if node := r.ContainsCallExpr(n, c); node != nil {
38+
if node := r.ContainsCallExpr(n, c, false); node != nil {
3939
if r.ResolveVar(node, c) {
4040
return gosec.NewIssue(c, n, r.ID(), r.What, r.Severity, r.Confidence), nil
4141
}

0 commit comments

Comments
 (0)