Skip to content
Closed
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@

# Go workspace file
go.work

# Go module cache files in testdata
**/testdata/pkg/mod/
192 changes: 185 additions & 7 deletions sql/injection/injection.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,146 @@ var injectableSQLMethods = taint.NewSinks(
"(*github.com/jinzhu/gorm.DB).Raw",
"(*github.com/jinzhu/gorm.DB).Exec",
"(*github.com/jinzhu/gorm.DB).Order",
//
// TODO: add more, consider (non-)pointer variants?
"(*github.com/jinzhu/gorm.DB).Table",
// GORM v2
// https://gorm.io/docs/security.html
"(*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",
"(*gorm.io/gorm.DB).Table",
// 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",
"(*github.com/go-gorm/gorm.DB).Table",
// XORM
// https://xorm.io/docs/
"(*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",
// sqlx
// https://github.com/jmoiron/sqlx
"(*github.com/jmoiron/sqlx.DB).Select",
"(*github.com/jmoiron/sqlx.DB).Get",
"(*github.com/jmoiron/sqlx.DB).MustExec",
"(*github.com/jmoiron/sqlx.DB).Queryx",
"(*github.com/jmoiron/sqlx.DB).NamedExec",
"(*github.com/jmoiron/sqlx.DB).NamedQuery",
"(*github.com/jmoiron/sqlx.Tx).Select",
"(*github.com/jmoiron/sqlx.Tx).Get",
"(*github.com/jmoiron/sqlx.Tx).MustExec",
"(*github.com/jmoiron/sqlx.Tx).Queryx",
"(*github.com/jmoiron/sqlx.Tx).NamedExec",
"(*github.com/jmoiron/sqlx.Tx).NamedQuery",
// Squirrel query builder
// https://github.com/Masterminds/squirrel
"github.com/Masterminds/squirrel.Expr",
"gopkg.in/Masterminds/squirrel.v1.Expr",
"github.com/lann/squirrel.Expr",
// go-pg
// https://github.com/go-pg/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
// https://github.com/rqlite/gorqlite
"(*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",
)

// Analyzer finds potential SQL injection issues to demonstrate
Expand Down Expand Up @@ -99,11 +237,26 @@ func imports(pass *analysis.Pass, pkgs ...string) bool {
}

func run(pass *analysis.Pass) (interface{}, error) {
// Require the database/sql or GORM v1 packages are imported in the
// Require at least one SQL package is 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") {
sqlPackages := []string{
"database/sql",
"github.com/jinzhu/gorm", // GORM v1
"gorm.io/gorm", // GORM v2
"github.com/go-gorm/gorm", // GORM v2 alternative path
"xorm.io/xorm", // XORM
"github.com/go-xorm/xorm", // XORM alternative path
"github.com/jmoiron/sqlx", // sqlx
"github.com/Masterminds/squirrel", // Squirrel
"gopkg.in/Masterminds/squirrel.v1", // Squirrel v1
"github.com/lann/squirrel", // Squirrel alternative
"github.com/go-pg/pg", // go-pg
"github.com/rqlite/gorqlite", // rqlite
"github.com/raindog308/gorqlite", // rqlite alternative
Comment on lines +244 to +257
Copy link

Copilot AI Jul 4, 2025

Choose a reason for hiding this comment

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

[nitpick] The sqlPackages slice is defined inline in run. Consider moving this list to a package-level constant or var to improve readability and ease future additions.

Suggested change
sqlPackages := []string{
"database/sql",
"github.com/jinzhu/gorm", // GORM v1
"gorm.io/gorm", // GORM v2
"github.com/go-gorm/gorm", // GORM v2 alternative path
"xorm.io/xorm", // XORM
"github.com/go-xorm/xorm", // XORM alternative path
"github.com/jmoiron/sqlx", // sqlx
"github.com/Masterminds/squirrel", // Squirrel
"gopkg.in/Masterminds/squirrel.v1", // Squirrel v1
"github.com/lann/squirrel", // Squirrel alternative
"github.com/go-pg/pg", // go-pg
"github.com/rqlite/gorqlite", // rqlite
"github.com/raindog308/gorqlite", // rqlite alternative
if !imports(pass, sqlPackages...) {
return nil, nil

Copilot uses AI. Check for mistakes.
}
if !imports(pass, sqlPackages...) {
return nil, nil
}

Expand Down Expand Up @@ -170,16 +323,41 @@ 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:]
// Determine the method/function being called
methodName := queryEdge.Site.Value().Call.Value.String()

// Get the query arguments. For methods, skip the first element (receiver).
// For functions, start from the beginning.
var queryArgs []ssa.Value
if strings.Contains(methodName, "(*") { // Method call
queryArgs = queryEdge.Site.Common().Args[1:]
} else { // Function call
queryArgs = queryEdge.Site.Common().Args
}

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

Copilot AI Jul 4, 2025

Choose a reason for hiding this comment

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

The check for context variants uses HasPrefix("Context"), but context-aware methods like QueryContext end with "Context" rather than start. Consider using strings.Contains or strings.HasSuffix to properly detect and skip the first context argument.

Suggested change
if strings.HasPrefix(queryEdge.Site.Value().Call.Value.String(), "Context") {
if strings.HasSuffix(queryEdge.Site.Value().Call.Value.String(), "Context") {

Copilot uses AI. Check for mistakes.
queryArgs = queryArgs[1:]
}

// Determine the query argument position based on the method being called
var queryArgIndex int

// For sqlx Select and Get methods, the query is the second argument (index 1)
if strings.Contains(methodName, "sqlx") && (strings.Contains(methodName, "Select") || strings.Contains(methodName, "Get")) {
queryArgIndex = 1
} else {
// For most other methods, the query is the first argument (index 0)
queryArgIndex = 0
}

// Ensure we have enough arguments
if len(queryArgs) <= queryArgIndex {
continue
}

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

// Ensure it is a constant (prepared statement), otherwise report
// potential SQL injection.
Expand Down
22 changes: 22 additions & 0 deletions sql/injection/injection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,25 @@ func TestL(t *testing.T) {
func TestM(t *testing.T) {
analysistest.Run(t, testdata, Analyzer, "m")
}

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

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

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

func TestContextual(t *testing.T) {
// This test should pass with no diagnostics since no SQL packages are imported
analysistest.Run(t, testdata, Analyzer, "contextual")
}

// TODO: Fix Squirrel test - function call detection needs work
// func TestSquirrel(t *testing.T) {
// analysistest.Run(t, testdata, Analyzer, "squirrel")
// }
26 changes: 26 additions & 0 deletions sql/injection/testdata/src/contextual/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package main

import (
"net/http"
// Notice: no SQL packages imported
)

func businessNoSQL(userInput string) {
// This should not be analyzed since no SQL packages are imported
// Even though we have a function that looks like it could be vulnerable
_ = userInput
}

func run() {
mux := http.NewServeMux()

mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
businessNoSQL(r.URL.Query().Get("sql-query"))
})

http.ListenAndServe(":8080", mux)
}

func main() {
run()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package squirrel

// Expr creates an expression for a SQL fragment
func Expr(sql string, args ...interface{}) interface{} {
return nil
}
68 changes: 68 additions & 0 deletions sql/injection/testdata/src/github.com/jmoiron/sqlx/mock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package sqlx

// DB is mocked from https://github.com/jmoiron/sqlx
type DB struct{}

// Tx is mocked from https://github.com/jmoiron/sqlx
type Tx struct{}

// Select is a method that can be vulnerable to SQL injection
func (db *DB) Select(dest interface{}, query string, args ...interface{}) error {
return nil
}

// Get is a method that can be vulnerable to SQL injection
func (db *DB) Get(dest interface{}, query string, args ...interface{}) error {
return nil
}

// MustExec is a method that can be vulnerable to SQL injection
func (db *DB) MustExec(query string, args ...interface{}) {
}

// Queryx is a method that can be vulnerable to SQL injection
func (db *DB) Queryx(query string, args ...interface{}) *Rows {
return nil
}

// NamedExec is a method that can be vulnerable to SQL injection
func (db *DB) NamedExec(query string, arg interface{}) error {
return nil
}

// NamedQuery is a method that can be vulnerable to SQL injection
func (db *DB) NamedQuery(query string, arg interface{}) (*Rows, error) {
return nil, nil
}

// Select is a method that can be vulnerable to SQL injection
func (tx *Tx) Select(dest interface{}, query string, args ...interface{}) error {
return nil
}

// Get is a method that can be vulnerable to SQL injection
func (tx *Tx) Get(dest interface{}, query string, args ...interface{}) error {
return nil
}

// MustExec is a method that can be vulnerable to SQL injection
func (tx *Tx) MustExec(query string, args ...interface{}) {
}

// Queryx is a method that can be vulnerable to SQL injection
func (tx *Tx) Queryx(query string, args ...interface{}) *Rows {
return nil
}

// NamedExec is a method that can be vulnerable to SQL injection
func (tx *Tx) NamedExec(query string, arg interface{}) error {
return nil
}

// NamedQuery is a method that can be vulnerable to SQL injection
func (tx *Tx) NamedQuery(query string, arg interface{}) (*Rows, error) {
return nil, nil
}

// Rows is a mock struct
type Rows struct{}
Loading