Skip to content

Commit 1f58616

Browse files
authored
Expand SQLi sinks (#41)
1 parent bdda215 commit 1f58616

File tree

25 files changed

+632
-20
lines changed

25 files changed

+632
-20
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,20 @@ $ go install github.com/picatz/taint/cmd/taint@latest
8181

8282
The `sqli` [analyzer](https://pkg.go.dev/golang.org/x/tools/go/analysis#Analyzer) finds potential SQL injections.
8383

84+
Supported SQL packages include:
85+
86+
- the standard library `database/sql` package
87+
- `github.com/jinzhu/gorm` (GORM v1)
88+
- `gorm.io/gorm` (GORM v2)
89+
- `github.com/jmoiron/sqlx`
90+
- `github.com/go-gorm/gorm` (GORM v2 alt)
91+
- `xorm.io/xorm` and `github.com/go-xorm/xorm`
92+
- `github.com/go-pg/pg`
93+
- `github.com/rqlite/gorqlite`
94+
- `github.com/raindog308/gorqlite`
95+
- `github.com/Masterminds/squirrel` and variants
96+
- database drivers like `github.com/mattn/go-sqlite3`
97+
8498
```console
8599
$ go install github.com/picatz/taint/cmd/sqli@latest
86100
```

sql/injection/injection.go

Lines changed: 195 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package injection
22

33
import (
44
"fmt"
5+
"go/types"
56
"strings"
67

78
"github.com/picatz/taint"
@@ -15,13 +16,17 @@ import (
1516
// userControlledValues are the sources of user controlled values that
1617
// can be tained and end up in a SQL query.
1718
var userControlledValues = taint.NewSources(
18-
// Function (and method) calls
19+
// Function (and method) calls that are user controlled
20+
// over the netork. These are all taken into account as
21+
// part of *net/http.Request, but are listed here for
22+
// demonstration purposes.
23+
//
1924
// "(net/url.Values).Get",
2025
// "(*net/url.URL).Query",
2126
// "(*net/url.URL).Redacted",
2227
// "(*net/url.URL).EscapedFragment",
2328
// "(*net/url.Userinfo).Username",
24-
// "(*net/url.Userinfo).Passworde",
29+
// "(*net/url.Userinfo).Password",
2530
// "(*net/url.Userinfo).String",
2631
// "(*net/http.Request).FormFile",
2732
// "(*net/http.Request).FormValue",
@@ -39,7 +44,7 @@ var userControlledValues = taint.NewSources(
3944
//
4045
// TODO: add more, consider pointer variants and specific fields on types
4146
// TODO: consider support for protobuf defined *Request types...
42-
// TODO: consider supprot for gRPC request metadata (HTTP2 headers)
47+
// TODO: consider support for gRPC request metadata (HTTP2 headers)
4348
// TODO: consider support for msgpack-rpc?
4449
)
4550

@@ -68,6 +73,139 @@ var injectableSQLMethods = taint.NewSinks(
6873
"(*github.com/jinzhu/gorm.DB).Raw",
6974
"(*github.com/jinzhu/gorm.DB).Exec",
7075
"(*github.com/jinzhu/gorm.DB).Order",
76+
// GORM v2
77+
"(*gorm.io/gorm.DB).Where",
78+
"(*gorm.io/gorm.DB).Or",
79+
"(*gorm.io/gorm.DB).Not",
80+
"(*gorm.io/gorm.DB).Group",
81+
"(*gorm.io/gorm.DB).Having",
82+
"(*gorm.io/gorm.DB).Joins",
83+
"(*gorm.io/gorm.DB).Select",
84+
"(*gorm.io/gorm.DB).Distinct",
85+
"(*gorm.io/gorm.DB).Pluck",
86+
"(*gorm.io/gorm.DB).Raw",
87+
"(*gorm.io/gorm.DB).Exec",
88+
"(*gorm.io/gorm.DB).Order",
89+
// Alternative GORM v2 import path
90+
"(*github.com/go-gorm/gorm.DB).Where",
91+
"(*github.com/go-gorm/gorm.DB).Or",
92+
"(*github.com/go-gorm/gorm.DB).Not",
93+
"(*github.com/go-gorm/gorm.DB).Group",
94+
"(*github.com/go-gorm/gorm.DB).Having",
95+
"(*github.com/go-gorm/gorm.DB).Joins",
96+
"(*github.com/go-gorm/gorm.DB).Select",
97+
"(*github.com/go-gorm/gorm.DB).Distinct",
98+
"(*github.com/go-gorm/gorm.DB).Pluck",
99+
"(*github.com/go-gorm/gorm.DB).Raw",
100+
"(*github.com/go-gorm/gorm.DB).Exec",
101+
"(*github.com/go-gorm/gorm.DB).Order",
102+
// sqlx
103+
"(*github.com/jmoiron/sqlx.DB).Queryx",
104+
"(*github.com/jmoiron/sqlx.DB).QueryRowx",
105+
"(*github.com/jmoiron/sqlx.DB).Query",
106+
"(*github.com/jmoiron/sqlx.DB).QueryRow",
107+
"(*github.com/jmoiron/sqlx.DB).Select",
108+
"(*github.com/jmoiron/sqlx.DB).Get",
109+
"(*github.com/jmoiron/sqlx.DB).Exec",
110+
"(*github.com/jmoiron/sqlx.Tx).Queryx",
111+
"(*github.com/jmoiron/sqlx.Tx).QueryRowx",
112+
"(*github.com/jmoiron/sqlx.Tx).Query",
113+
"(*github.com/jmoiron/sqlx.Tx).QueryRow",
114+
"(*github.com/jmoiron/sqlx.Tx).Select",
115+
"(*github.com/jmoiron/sqlx.Tx).Get",
116+
"(*github.com/jmoiron/sqlx.Tx).Exec",
117+
// xorm
118+
"(*xorm.io/xorm.Engine).Query",
119+
"(*xorm.io/xorm.Engine).Exec",
120+
"(*xorm.io/xorm.Engine).QueryString",
121+
"(*xorm.io/xorm.Engine).QueryInterface",
122+
"(*xorm.io/xorm.Engine).SQL",
123+
"(*xorm.io/xorm.Engine).Where",
124+
"(*xorm.io/xorm.Engine).And",
125+
"(*xorm.io/xorm.Engine).Or",
126+
"(*xorm.io/xorm.Engine).Alias",
127+
"(*xorm.io/xorm.Engine).NotIn",
128+
"(*xorm.io/xorm.Engine).In",
129+
"(*xorm.io/xorm.Engine).Select",
130+
"(*xorm.io/xorm.Engine).SetExpr",
131+
"(*xorm.io/xorm.Engine).OrderBy",
132+
"(*xorm.io/xorm.Engine).Having",
133+
"(*xorm.io/xorm.Engine).GroupBy",
134+
"(*xorm.io/xorm.Engine).Join",
135+
"(*xorm.io/xorm.Session).Query",
136+
"(*xorm.io/xorm.Session).Exec",
137+
"(*xorm.io/xorm.Session).QueryString",
138+
"(*xorm.io/xorm.Session).QueryInterface",
139+
"(*xorm.io/xorm.Session).SQL",
140+
"(*xorm.io/xorm.Session).Where",
141+
"(*xorm.io/xorm.Session).And",
142+
"(*xorm.io/xorm.Session).Or",
143+
"(*xorm.io/xorm.Session).Alias",
144+
"(*xorm.io/xorm.Session).NotIn",
145+
"(*xorm.io/xorm.Session).In",
146+
"(*xorm.io/xorm.Session).Select",
147+
"(*xorm.io/xorm.Session).SetExpr",
148+
"(*xorm.io/xorm.Session).OrderBy",
149+
"(*xorm.io/xorm.Session).Having",
150+
"(*xorm.io/xorm.Session).GroupBy",
151+
"(*xorm.io/xorm.Session).Join",
152+
// Alternative xorm import path
153+
"(*github.com/go-xorm/xorm.Engine).Query",
154+
"(*github.com/go-xorm/xorm.Engine).Exec",
155+
"(*github.com/go-xorm/xorm.Engine).QueryString",
156+
"(*github.com/go-xorm/xorm.Engine).QueryInterface",
157+
"(*github.com/go-xorm/xorm.Engine).SQL",
158+
"(*github.com/go-xorm/xorm.Engine).Where",
159+
"(*github.com/go-xorm/xorm.Engine).And",
160+
"(*github.com/go-xorm/xorm.Engine).Or",
161+
"(*github.com/go-xorm/xorm.Engine).Alias",
162+
"(*github.com/go-xorm/xorm.Engine).NotIn",
163+
"(*github.com/go-xorm/xorm.Engine).In",
164+
"(*github.com/go-xorm/xorm.Engine).Select",
165+
"(*github.com/go-xorm/xorm.Engine).SetExpr",
166+
"(*github.com/go-xorm/xorm.Engine).OrderBy",
167+
"(*github.com/go-xorm/xorm.Engine).Having",
168+
"(*github.com/go-xorm/xorm.Engine).GroupBy",
169+
"(*github.com/go-xorm/xorm.Engine).Join",
170+
"(*github.com/go-xorm/xorm.Session).Query",
171+
"(*github.com/go-xorm/xorm.Session).Exec",
172+
"(*github.com/go-xorm/xorm.Session).QueryString",
173+
"(*github.com/go-xorm/xorm.Session).QueryInterface",
174+
"(*github.com/go-xorm/xorm.Session).SQL",
175+
"(*github.com/go-xorm/xorm.Session).Where",
176+
"(*github.com/go-xorm/xorm.Session).And",
177+
"(*github.com/go-xorm/xorm.Session).Or",
178+
"(*github.com/go-xorm/xorm.Session).Alias",
179+
"(*github.com/go-xorm/xorm.Session).NotIn",
180+
"(*github.com/go-xorm/xorm.Session).In",
181+
"(*github.com/go-xorm/xorm.Session).Select",
182+
"(*github.com/go-xorm/xorm.Session).SetExpr",
183+
"(*github.com/go-xorm/xorm.Session).OrderBy",
184+
"(*github.com/go-xorm/xorm.Session).Having",
185+
"(*github.com/go-xorm/xorm.Session).GroupBy",
186+
"(*github.com/go-xorm/xorm.Session).Join",
187+
// go-pg
188+
"(*github.com/go-pg/pg.DB).Query",
189+
"(*github.com/go-pg/pg.DB).QueryOne",
190+
"(*github.com/go-pg/pg.DB).Exec",
191+
"(*github.com/go-pg/pg.DB).ExecOne",
192+
"(*github.com/go-pg/pg.Tx).Query",
193+
"(*github.com/go-pg/pg.Tx).QueryOne",
194+
"(*github.com/go-pg/pg.Tx).Exec",
195+
"(*github.com/go-pg/pg.Tx).ExecOne",
196+
// rqlite
197+
"(*github.com/rqlite/gorqlite.Connection).Query",
198+
"(*github.com/rqlite/gorqlite.Connection).QueryOne",
199+
"(*github.com/rqlite/gorqlite.Connection).Write",
200+
"(*github.com/rqlite/gorqlite.Connection).WriteOne",
201+
"(*github.com/raindog308/gorqlite.Connection).Query",
202+
"(*github.com/raindog308/gorqlite.Connection).QueryOne",
203+
"(*github.com/raindog308/gorqlite.Connection).Write",
204+
"(*github.com/raindog308/gorqlite.Connection).WriteOne",
205+
// Squirrel
206+
"github.com/Masterminds/squirrel.Expr",
207+
"gopkg.in/Masterminds/squirrel.v1.Expr",
208+
"github.com/lann/squirrel.Expr",
71209
//
72210
// TODO: add more, consider (non-)pointer variants?
73211
)
@@ -83,27 +221,50 @@ var Analyzer = &analysis.Analyzer{
83221

84222
// imports returns true if the package imports any of the given packages.
85223
func imports(pass *analysis.Pass, pkgs ...string) bool {
86-
var imported bool
87-
for _, imp := range pass.Pkg.Imports() {
224+
visited := make(map[*types.Package]bool)
225+
var walk func(*types.Package) bool
226+
walk = func(p *types.Package) bool {
227+
if visited[p] {
228+
return false
229+
}
230+
visited[p] = true
88231
for _, pkg := range pkgs {
89-
if strings.HasSuffix(imp.Path(), pkg) {
90-
imported = true
91-
break
232+
if strings.HasSuffix(p.Path(), pkg) {
233+
return true
92234
}
93235
}
94-
if imported {
95-
break
236+
for _, imp := range p.Imports() {
237+
if walk(imp) {
238+
return true
239+
}
96240
}
241+
return false
97242
}
98-
return imported
243+
return walk(pass.Pkg)
244+
}
245+
246+
var supportedSQLPackages = []string{
247+
"database/sql",
248+
"github.com/mattn/go-sqlite3",
249+
"github.com/jinzhu/gorm",
250+
"gorm.io/gorm",
251+
"github.com/go-gorm/gorm",
252+
"github.com/jmoiron/sqlx",
253+
"xorm.io/xorm",
254+
"github.com/go-xorm/xorm",
255+
"github.com/go-pg/pg",
256+
"github.com/rqlite/gorqlite",
257+
"github.com/raindog308/gorqlite",
258+
"github.com/Masterminds/squirrel",
259+
"gopkg.in/Masterminds/squirrel.v1",
260+
"github.com/lann/squirrel",
99261
}
100262

101263
func run(pass *analysis.Pass) (interface{}, error) {
102-
// Require the database/sql or GORM v1 packages are imported in the
103-
// program being analyzed before running the analysis.
104-
//
105-
// This prevents wasting time analyzing programs that don't use SQL.
106-
if !imports(pass, "database/sql", "github.com/jinzhu/gorm") {
264+
// Require at least one supported SQL package to be imported before
265+
// running the analysis. This avoids wasting time analyzing programs
266+
// that do not use SQL.
267+
if !imports(pass, supportedSQLPackages...) {
107268
return nil, nil
108269
}
109270

@@ -132,7 +293,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
132293
// Today, I believe the callgraphutil package is the most
133294
// accurate, but I'd love to be proven wrong.
134295

135-
// Note: this actually panis for testcase b
296+
// Note: this actually panics for testcase b
136297
// ptares, err := pointer.Analyze(&pointer.Config{
137298
// Mains: []*ssa.Package{buildSSA.Pkg},
138299
// BuildCallGraph: true,
@@ -170,14 +331,28 @@ func run(pass *analysis.Pass) (interface{}, error) {
170331
// (first argument after context).
171332
queryEdge := result.Path[len(result.Path)-1]
172333

173-
// Get the query arguments, skipping the first element, pointer to the DB.
174-
queryArgs := queryEdge.Site.Common().Args[1:]
334+
// Get the query arguments. If the sink is a method call, the
335+
// first argument is the receiver, which we skip.
336+
queryArgs := queryEdge.Site.Common().Args
337+
if queryEdge.Site.Common().Signature().Recv() != nil {
338+
if len(queryArgs) < 1 {
339+
continue
340+
}
341+
queryArgs = queryArgs[1:]
342+
}
175343

176344
// Skip the context argument, if using a *Context query variant.
177-
if strings.HasPrefix(queryEdge.Site.Value().Call.Value.String(), "Context") {
345+
if strings.HasSuffix(queryEdge.Site.Value().Call.Value.String(), "Context") {
346+
if len(queryArgs) < 2 {
347+
continue
348+
}
178349
queryArgs = queryArgs[1:]
179350
}
180351

352+
if len(queryArgs) == 0 {
353+
continue
354+
}
355+
181356
// Get the query function parameter.
182357
query := queryArgs[0]
183358

sql/injection/injection_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,39 @@ func TestL(t *testing.T) {
6565
func TestM(t *testing.T) {
6666
analysistest.Run(t, testdata, Analyzer, "m")
6767
}
68+
69+
func TestN(t *testing.T) {
70+
analysistest.Run(t, testdata, Analyzer, "n")
71+
}
72+
73+
func TestO(t *testing.T) {
74+
analysistest.Run(t, testdata, Analyzer, "o")
75+
}
76+
77+
func TestP(t *testing.T) {
78+
analysistest.Run(t, testdata, Analyzer, "p")
79+
}
80+
81+
func TestQ(t *testing.T) {
82+
analysistest.Run(t, testdata, Analyzer, "q")
83+
}
84+
85+
func TestR(t *testing.T) {
86+
analysistest.Run(t, testdata, Analyzer, "r")
87+
}
88+
89+
func TestS(t *testing.T) {
90+
analysistest.Run(t, testdata, Analyzer, "s")
91+
}
92+
93+
func TestT(t *testing.T) {
94+
analysistest.Run(t, testdata, Analyzer, "t")
95+
}
96+
97+
func TestU(t *testing.T) {
98+
analysistest.Run(t, testdata, Analyzer, "u")
99+
}
100+
101+
func TestV(t *testing.T) {
102+
analysistest.Run(t, testdata, Analyzer, "v")
103+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package squirrel
2+
3+
type Sqlizer interface{}
4+
5+
func Expr(sql string, args ...interface{}) Sqlizer { return nil }
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package gorm
2+
3+
type Model struct{}
4+
5+
type DB struct{}
6+
7+
type Config struct{}
8+
9+
type Dialector interface{}
10+
11+
func Open(d Dialector, config *Config) (*DB, error) { return nil, nil }
12+
13+
func (s *DB) Where(query interface{}, args ...interface{}) *DB { return nil }
14+
func (s *DB) Or(query interface{}, args ...interface{}) *DB { return nil }
15+
func (s *DB) Not(query interface{}, args ...interface{}) *DB { return nil }
16+
func (s *DB) Group(query string) *DB { return nil }
17+
func (s *DB) Having(query interface{}, args ...interface{}) *DB { return nil }
18+
func (s *DB) Joins(query string, args ...interface{}) *DB { return nil }
19+
func (s *DB) Select(query interface{}, args ...interface{}) *DB { return nil }
20+
func (s *DB) Distinct(args ...interface{}) *DB { return nil }
21+
func (s *DB) Pluck(column string, value interface{}) *DB { return nil }
22+
func (s *DB) Raw(sql string, values ...interface{}) *DB { return nil }
23+
func (s *DB) Exec(sql string, values ...interface{}) *DB { return nil }
24+
func (s *DB) Order(value interface{}) *DB { return nil }
25+
func (s *DB) Find(dest interface{}, conds ...interface{}) *DB { return nil }

0 commit comments

Comments
 (0)