Skip to content
Merged
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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,20 @@ $ go install github.com/picatz/taint/cmd/taint@latest

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

Supported SQL packages include:

- the standard library `database/sql` package
- `github.com/jinzhu/gorm` (GORM v1)
- `gorm.io/gorm` (GORM v2)
- `github.com/jmoiron/sqlx`
- `github.com/go-gorm/gorm` (GORM v2 alt)
- `xorm.io/xorm` and `github.com/go-xorm/xorm`
- `github.com/go-pg/pg`
- `github.com/rqlite/gorqlite`
- `github.com/raindog308/gorqlite`
- `github.com/Masterminds/squirrel` and variants
- database drivers like `github.com/mattn/go-sqlite3`

```console
$ go install github.com/picatz/taint/cmd/sqli@latest
```
Expand Down
215 changes: 195 additions & 20 deletions sql/injection/injection.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package injection

import (
"fmt"
"go/types"
"strings"

"github.com/picatz/taint"
Expand All @@ -15,13 +16,17 @@ import (
// userControlledValues are the sources of user controlled values that
// can be tained and end up in a SQL query.
var userControlledValues = taint.NewSources(
// Function (and method) calls
// Function (and method) calls that are user controlled
// over the netork. These are all taken into account as
Copy link

Copilot AI Jul 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix spelling of 'netork' to 'network'.

Suggested change
// over the netork. These are all taken into account as
// over the network. These are all taken into account as

Copilot uses AI. Check for mistakes.
// part of *net/http.Request, but are listed here for
// demonstration purposes.
//
// "(net/url.Values).Get",
// "(*net/url.URL).Query",
// "(*net/url.URL).Redacted",
// "(*net/url.URL).EscapedFragment",
// "(*net/url.Userinfo).Username",
// "(*net/url.Userinfo).Passworde",
// "(*net/url.Userinfo).Password",
// "(*net/url.Userinfo).String",
// "(*net/http.Request).FormFile",
// "(*net/http.Request).FormValue",
Expand All @@ -39,7 +44,7 @@ var userControlledValues = taint.NewSources(
//
// TODO: add more, consider pointer variants and specific fields on types
// TODO: consider support for protobuf defined *Request types...
// TODO: consider supprot for gRPC request metadata (HTTP2 headers)
// TODO: consider support for gRPC request metadata (HTTP2 headers)
// TODO: consider support for msgpack-rpc?
)

Expand Down Expand Up @@ -68,6 +73,139 @@ var injectableSQLMethods = taint.NewSinks(
"(*github.com/jinzhu/gorm.DB).Raw",
"(*github.com/jinzhu/gorm.DB).Exec",
"(*github.com/jinzhu/gorm.DB).Order",
// GORM v2
Copy link

Copilot AI Jul 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The injectableSQLMethods list is growing large and repetitive; consider grouping method patterns per package or generating this list programmatically to simplify future maintenance.

Copilot uses AI. Check for mistakes.
"(*gorm.io/gorm.DB).Where",
"(*gorm.io/gorm.DB).Or",
"(*gorm.io/gorm.DB).Not",
"(*gorm.io/gorm.DB).Group",
"(*gorm.io/gorm.DB).Having",
"(*gorm.io/gorm.DB).Joins",
"(*gorm.io/gorm.DB).Select",
"(*gorm.io/gorm.DB).Distinct",
"(*gorm.io/gorm.DB).Pluck",
"(*gorm.io/gorm.DB).Raw",
"(*gorm.io/gorm.DB).Exec",
"(*gorm.io/gorm.DB).Order",
// Alternative GORM v2 import path
"(*github.com/go-gorm/gorm.DB).Where",
"(*github.com/go-gorm/gorm.DB).Or",
"(*github.com/go-gorm/gorm.DB).Not",
"(*github.com/go-gorm/gorm.DB).Group",
"(*github.com/go-gorm/gorm.DB).Having",
"(*github.com/go-gorm/gorm.DB).Joins",
"(*github.com/go-gorm/gorm.DB).Select",
"(*github.com/go-gorm/gorm.DB).Distinct",
"(*github.com/go-gorm/gorm.DB).Pluck",
"(*github.com/go-gorm/gorm.DB).Raw",
"(*github.com/go-gorm/gorm.DB).Exec",
"(*github.com/go-gorm/gorm.DB).Order",
// sqlx
"(*github.com/jmoiron/sqlx.DB).Queryx",
"(*github.com/jmoiron/sqlx.DB).QueryRowx",
"(*github.com/jmoiron/sqlx.DB).Query",
"(*github.com/jmoiron/sqlx.DB).QueryRow",
"(*github.com/jmoiron/sqlx.DB).Select",
"(*github.com/jmoiron/sqlx.DB).Get",
"(*github.com/jmoiron/sqlx.DB).Exec",
"(*github.com/jmoiron/sqlx.Tx).Queryx",
"(*github.com/jmoiron/sqlx.Tx).QueryRowx",
"(*github.com/jmoiron/sqlx.Tx).Query",
"(*github.com/jmoiron/sqlx.Tx).QueryRow",
"(*github.com/jmoiron/sqlx.Tx).Select",
"(*github.com/jmoiron/sqlx.Tx).Get",
"(*github.com/jmoiron/sqlx.Tx).Exec",
// xorm
"(*xorm.io/xorm.Engine).Query",
"(*xorm.io/xorm.Engine).Exec",
"(*xorm.io/xorm.Engine).QueryString",
"(*xorm.io/xorm.Engine).QueryInterface",
"(*xorm.io/xorm.Engine).SQL",
"(*xorm.io/xorm.Engine).Where",
"(*xorm.io/xorm.Engine).And",
"(*xorm.io/xorm.Engine).Or",
"(*xorm.io/xorm.Engine).Alias",
"(*xorm.io/xorm.Engine).NotIn",
"(*xorm.io/xorm.Engine).In",
"(*xorm.io/xorm.Engine).Select",
"(*xorm.io/xorm.Engine).SetExpr",
"(*xorm.io/xorm.Engine).OrderBy",
"(*xorm.io/xorm.Engine).Having",
"(*xorm.io/xorm.Engine).GroupBy",
"(*xorm.io/xorm.Engine).Join",
"(*xorm.io/xorm.Session).Query",
"(*xorm.io/xorm.Session).Exec",
"(*xorm.io/xorm.Session).QueryString",
"(*xorm.io/xorm.Session).QueryInterface",
"(*xorm.io/xorm.Session).SQL",
"(*xorm.io/xorm.Session).Where",
"(*xorm.io/xorm.Session).And",
"(*xorm.io/xorm.Session).Or",
"(*xorm.io/xorm.Session).Alias",
"(*xorm.io/xorm.Session).NotIn",
"(*xorm.io/xorm.Session).In",
"(*xorm.io/xorm.Session).Select",
"(*xorm.io/xorm.Session).SetExpr",
"(*xorm.io/xorm.Session).OrderBy",
"(*xorm.io/xorm.Session).Having",
"(*xorm.io/xorm.Session).GroupBy",
"(*xorm.io/xorm.Session).Join",
// Alternative xorm import path
"(*github.com/go-xorm/xorm.Engine).Query",
"(*github.com/go-xorm/xorm.Engine).Exec",
"(*github.com/go-xorm/xorm.Engine).QueryString",
"(*github.com/go-xorm/xorm.Engine).QueryInterface",
"(*github.com/go-xorm/xorm.Engine).SQL",
"(*github.com/go-xorm/xorm.Engine).Where",
"(*github.com/go-xorm/xorm.Engine).And",
"(*github.com/go-xorm/xorm.Engine).Or",
"(*github.com/go-xorm/xorm.Engine).Alias",
"(*github.com/go-xorm/xorm.Engine).NotIn",
"(*github.com/go-xorm/xorm.Engine).In",
"(*github.com/go-xorm/xorm.Engine).Select",
"(*github.com/go-xorm/xorm.Engine).SetExpr",
"(*github.com/go-xorm/xorm.Engine).OrderBy",
"(*github.com/go-xorm/xorm.Engine).Having",
"(*github.com/go-xorm/xorm.Engine).GroupBy",
"(*github.com/go-xorm/xorm.Engine).Join",
"(*github.com/go-xorm/xorm.Session).Query",
"(*github.com/go-xorm/xorm.Session).Exec",
"(*github.com/go-xorm/xorm.Session).QueryString",
"(*github.com/go-xorm/xorm.Session).QueryInterface",
"(*github.com/go-xorm/xorm.Session).SQL",
"(*github.com/go-xorm/xorm.Session).Where",
"(*github.com/go-xorm/xorm.Session).And",
"(*github.com/go-xorm/xorm.Session).Or",
"(*github.com/go-xorm/xorm.Session).Alias",
"(*github.com/go-xorm/xorm.Session).NotIn",
"(*github.com/go-xorm/xorm.Session).In",
"(*github.com/go-xorm/xorm.Session).Select",
"(*github.com/go-xorm/xorm.Session).SetExpr",
"(*github.com/go-xorm/xorm.Session).OrderBy",
"(*github.com/go-xorm/xorm.Session).Having",
"(*github.com/go-xorm/xorm.Session).GroupBy",
"(*github.com/go-xorm/xorm.Session).Join",
// go-pg
"(*github.com/go-pg/pg.DB).Query",
"(*github.com/go-pg/pg.DB).QueryOne",
"(*github.com/go-pg/pg.DB).Exec",
"(*github.com/go-pg/pg.DB).ExecOne",
"(*github.com/go-pg/pg.Tx).Query",
"(*github.com/go-pg/pg.Tx).QueryOne",
"(*github.com/go-pg/pg.Tx).Exec",
"(*github.com/go-pg/pg.Tx).ExecOne",
// rqlite
"(*github.com/rqlite/gorqlite.Connection).Query",
"(*github.com/rqlite/gorqlite.Connection).QueryOne",
"(*github.com/rqlite/gorqlite.Connection).Write",
"(*github.com/rqlite/gorqlite.Connection).WriteOne",
"(*github.com/raindog308/gorqlite.Connection).Query",
"(*github.com/raindog308/gorqlite.Connection).QueryOne",
"(*github.com/raindog308/gorqlite.Connection).Write",
"(*github.com/raindog308/gorqlite.Connection).WriteOne",
// Squirrel
"github.com/Masterminds/squirrel.Expr",
"gopkg.in/Masterminds/squirrel.v1.Expr",
"github.com/lann/squirrel.Expr",
//
// TODO: add more, consider (non-)pointer variants?
)
Expand All @@ -83,27 +221,50 @@ var Analyzer = &analysis.Analyzer{

// imports returns true if the package imports any of the given packages.
func imports(pass *analysis.Pass, pkgs ...string) bool {
var imported bool
for _, imp := range pass.Pkg.Imports() {
visited := make(map[*types.Package]bool)
var walk func(*types.Package) bool
walk = func(p *types.Package) bool {
if visited[p] {
return false
}
visited[p] = true
for _, pkg := range pkgs {
if strings.HasSuffix(imp.Path(), pkg) {
imported = true
break
if strings.HasSuffix(p.Path(), pkg) {
Copy link

Copilot AI Jul 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider using exact import path matching (e.g., p.Path() == pkg) instead of suffix matching to avoid accidental false positives when package paths share common suffixes.

Suggested change
if strings.HasSuffix(p.Path(), pkg) {
if p.Path() == pkg {

Copilot uses AI. Check for mistakes.
return true
}
}
if imported {
break
for _, imp := range p.Imports() {
if walk(imp) {
return true
}
}
return false
}
return imported
return walk(pass.Pkg)
}

var supportedSQLPackages = []string{
"database/sql",
"github.com/mattn/go-sqlite3",
"github.com/jinzhu/gorm",
"gorm.io/gorm",
"github.com/go-gorm/gorm",
"github.com/jmoiron/sqlx",
"xorm.io/xorm",
"github.com/go-xorm/xorm",
"github.com/go-pg/pg",
"github.com/rqlite/gorqlite",
"github.com/raindog308/gorqlite",
"github.com/Masterminds/squirrel",
"gopkg.in/Masterminds/squirrel.v1",
"github.com/lann/squirrel",
}

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

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

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

// Get the query arguments, skipping the first element, pointer to the DB.
queryArgs := queryEdge.Site.Common().Args[1:]
// Get the query arguments. If the sink is a method call, the
// first argument is the receiver, which we skip.
queryArgs := queryEdge.Site.Common().Args
if queryEdge.Site.Common().Signature().Recv() != nil {
if len(queryArgs) < 1 {
continue
}
queryArgs = queryArgs[1:]
}

// Skip the context argument, if using a *Context query variant.
if strings.HasPrefix(queryEdge.Site.Value().Call.Value.String(), "Context") {
if strings.HasSuffix(queryEdge.Site.Value().Call.Value.String(), "Context") {
Copy link

Copilot AI Jul 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Relying on string suffix matching may be brittle; consider inspecting the method's name or signature directly (e.g., using Signature().Name()) to accurately detect Context variants like QueryContext, ExecContext, etc.

Suggested change
if strings.HasSuffix(queryEdge.Site.Value().Call.Value.String(), "Context") {
if strings.Contains(queryEdge.Site.Common().Signature().Name(), "Context") {

Copilot uses AI. Check for mistakes.
if len(queryArgs) < 2 {
continue
}
queryArgs = queryArgs[1:]
}

if len(queryArgs) == 0 {
continue
}

// Get the query function parameter.
query := queryArgs[0]

Expand Down
36 changes: 36 additions & 0 deletions sql/injection/injection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,39 @@ func TestL(t *testing.T) {
func TestM(t *testing.T) {
analysistest.Run(t, testdata, Analyzer, "m")
}

func TestN(t *testing.T) {
analysistest.Run(t, testdata, Analyzer, "n")
}

func TestO(t *testing.T) {
analysistest.Run(t, testdata, Analyzer, "o")
}

func TestP(t *testing.T) {
analysistest.Run(t, testdata, Analyzer, "p")
}

func TestQ(t *testing.T) {
analysistest.Run(t, testdata, Analyzer, "q")
}

func TestR(t *testing.T) {
analysistest.Run(t, testdata, Analyzer, "r")
}

func TestS(t *testing.T) {
analysistest.Run(t, testdata, Analyzer, "s")
}

func TestT(t *testing.T) {
analysistest.Run(t, testdata, Analyzer, "t")
}

func TestU(t *testing.T) {
analysistest.Run(t, testdata, Analyzer, "u")
}

func TestV(t *testing.T) {
analysistest.Run(t, testdata, Analyzer, "v")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package squirrel

type Sqlizer interface{}

func Expr(sql string, args ...interface{}) Sqlizer { return nil }
25 changes: 25 additions & 0 deletions sql/injection/testdata/src/github.com/go-gorm/gorm/mock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package gorm

type Model struct{}

type DB struct{}

type Config struct{}

type Dialector interface{}

func Open(d Dialector, config *Config) (*DB, error) { return nil, nil }

func (s *DB) Where(query interface{}, args ...interface{}) *DB { return nil }
func (s *DB) Or(query interface{}, args ...interface{}) *DB { return nil }
func (s *DB) Not(query interface{}, args ...interface{}) *DB { return nil }
func (s *DB) Group(query string) *DB { return nil }
func (s *DB) Having(query interface{}, args ...interface{}) *DB { return nil }
func (s *DB) Joins(query string, args ...interface{}) *DB { return nil }
func (s *DB) Select(query interface{}, args ...interface{}) *DB { return nil }
func (s *DB) Distinct(args ...interface{}) *DB { return nil }
func (s *DB) Pluck(column string, value interface{}) *DB { return nil }
func (s *DB) Raw(sql string, values ...interface{}) *DB { return nil }
func (s *DB) Exec(sql string, values ...interface{}) *DB { return nil }
func (s *DB) Order(value interface{}) *DB { return nil }
func (s *DB) Find(dest interface{}, conds ...interface{}) *DB { return nil }
Loading