Compare commits

...

3 Commits

Author SHA1 Message Date
oittaa
649e2c8da4 remove deprecated ast.Object (#1455)
* remove deprecated ast.Object

* feat(tls): enhance TLS configuration handling with new checks for InsecureSkipVerify and PreferServerCipherSuites

---------

Co-authored-by: Cosmin Cojocar <cosmin@cojocar.ch>
2026-01-06 14:44:42 +01:00
oittaa
35a92b49d5 feat(sql): enhance SQL injection detection with improved string concatenation checks (#1454)
* feat(sql): enhance SQL injection detection with improved string concatenation checks

* optimize: only one ast.Inspect loop, use slices.ContainsFunc

* refactor(sql): streamline SQL argument retrieval, replace constObject with TryResolve, minor cleanup

* feat(sql): enhance query mutation checks for shadowed variables and add regression tests

* remove deprecated ast.Object
2026-01-06 14:30:54 +01:00
oittaa
bc9d2bc879 feat(rules): enhance subprocess variable checks (#1453)
* feat(rules): enhance subprocess variable checks

* ast.Object is deprecated
2026-01-06 14:20:23 +01:00
13 changed files with 1184 additions and 589 deletions

View File

@@ -385,29 +385,44 @@ func GetPkgAbsPath(pkgPath string) (string, error) {
return absPath, nil
}
// ConcatString recursively concatenates strings from a binary expression
func ConcatString(n *ast.BinaryExpr) (string, bool) {
var s string
// sub expressions are found in X object, Y object is always last BasicLit
if rightOperand, ok := n.Y.(*ast.BasicLit); ok {
if str, err := GetString(rightOperand); err == nil {
s = str + s
}
} else {
// ConcatString recursively concatenates constant strings from an expression
// if the entire chain is fully constant-derived (using TryResolve).
// Returns the concatenated string and true if successful.
func ConcatString(expr ast.Expr, ctx *Context) (string, bool) {
if expr == nil || !TryResolve(expr, ctx) {
return "", false
}
if leftOperand, ok := n.X.(*ast.BinaryExpr); ok {
if recursion, ok := ConcatString(leftOperand); ok {
s = recursion + s
var build strings.Builder
var traverse func(ast.Expr) bool
traverse = func(e ast.Expr) bool {
switch node := e.(type) {
case *ast.BasicLit:
if str, err := GetString(node); err == nil {
build.WriteString(str)
return true
}
return false
case *ast.Ident:
values := GetIdentStringValuesRecursive(node)
for _, v := range values {
build.WriteString(v)
}
return len(values) > 0
case *ast.BinaryExpr:
if node.Op != token.ADD {
return false
}
return traverse(node.X) && traverse(node.Y)
default:
return false
}
} else if leftOperand, ok := n.X.(*ast.BasicLit); ok {
if str, err := GetString(leftOperand); err == nil {
s = str + s
}
} else {
return "", false
}
return s, true
if traverse(expr) {
return build.String(), true
}
return "", false
}
// FindVarIdentities returns array of all variable identities in a given binary expression
@@ -574,3 +589,22 @@ func CLIBuildTags(buildTags []string) []string {
return buildFlags
}
// ContainingFile returns the *ast.File from ctx.PkgFiles that contains the given position provider.
// A position provider can be an ast.Node, a types.Object, or any type with a Pos() token.Pos method.
// Returns nil if not found or if the provider is nil/invalid.
func ContainingFile(p interface{ Pos() token.Pos }, ctx *Context) *ast.File {
if p == nil {
return nil
}
pos := p.Pos()
if !pos.IsValid() {
return nil
}
for _, f := range ctx.PkgFiles {
if f.Pos() <= pos && pos < f.End() {
return f
}
}
return nil
}

View File

@@ -2,7 +2,9 @@ package rules
import (
"go/ast"
"go/token"
"go/types"
"slices"
"github.com/securego/gosec/v2"
"github.com/securego/gosec/v2/issue"
@@ -18,29 +20,49 @@ func (a *archive) ID() string {
return a.MetaData.ID
}
// Match inspects AST nodes to determine if the filepath.Joins uses any argument derived from type zip.File or tar.Header
func (a *archive) Match(n ast.Node, c *gosec.Context) (*issue.Issue, error) {
if node := a.calls.ContainsPkgCallExpr(n, c, false); node != nil {
for _, arg := range node.Args {
var argType types.Type
if selector, ok := arg.(*ast.SelectorExpr); ok {
argType = c.Info.TypeOf(selector.X)
} else if ident, ok := arg.(*ast.Ident); ok {
if ident.Obj != nil && ident.Obj.Kind == ast.Var {
decl := ident.Obj.Decl
if assign, ok := decl.(*ast.AssignStmt); ok {
if selector, ok := assign.Rhs[0].(*ast.SelectorExpr); ok {
argType = c.Info.TypeOf(selector.X)
// getArchiveBaseType returns the underlying type (*archive/zip.File or *archive/tar.Header)
// if the expression is a direct .Name selector on such a type or a short-declared variable
// assigned from such a selector (e.g., name := file.Name).
func getArchiveBaseType(expr ast.Expr, ctx *gosec.Context, file *ast.File) types.Type {
switch e := expr.(type) {
case *ast.SelectorExpr:
return ctx.Info.TypeOf(e.X)
case *ast.Ident:
obj := ctx.Info.ObjectOf(e)
if v, ok := obj.(*types.Var); ok && file != nil {
var baseType types.Type
ast.Inspect(file, func(n ast.Node) bool {
if assign, ok := n.(*ast.AssignStmt); ok && assign.Tok == token.DEFINE {
for i, lhs := range assign.Lhs {
if id, ok := lhs.(*ast.Ident); ok &&
id.Pos() == v.Pos() && ctx.Info.ObjectOf(id) == v {
if i < len(assign.Rhs) {
if sel, ok := assign.Rhs[i].(*ast.SelectorExpr); ok {
baseType = ctx.Info.TypeOf(sel.X)
}
}
return false // Stop once defining assignment found
}
}
}
}
return true
})
return baseType
}
}
return nil
}
if argType != nil {
for _, t := range a.argTypes {
if argType.String() == t {
return c.NewIssue(n, a.ID(), a.What, a.Severity, a.Confidence), nil
}
// Match inspects AST nodes to determine if filepath.Join uses an argument derived
// from zip.File or tar.Header (typically the unsafe .Name field).
func (a *archive) Match(n ast.Node, ctx *gosec.Context) (*issue.Issue, error) {
if node := a.calls.ContainsPkgCallExpr(n, ctx, false); node != nil {
// All relevant variables are local (archive extraction context), so inspect the file containing the call
file := gosec.ContainingFile(node, ctx)
for _, arg := range node.Args {
if baseType := getArchiveBaseType(arg, ctx, file); baseType != nil {
if slices.Contains(a.argTypes, baseType.String()) {
return ctx.NewIssue(n, a.ID(), a.What, a.Severity, a.Confidence), nil
}
}
}
@@ -48,7 +70,7 @@ func (a *archive) Match(n ast.Node, c *gosec.Context) (*issue.Issue, error) {
return nil, nil
}
// NewArchive creates a new rule which detects the file traversal when extracting zip/tar archives
// NewArchive creates a new rule which detects file traversal when extracting zip/tar archives.
func NewArchive(id string, _ gosec.Config) (gosec.Rule, []ast.Node) {
calls := gosec.NewCallList()
calls.Add("path/filepath", "Join")

View File

@@ -17,6 +17,7 @@ package rules
import (
"fmt"
"go/ast"
"go/types"
"github.com/securego/gosec/v2"
"github.com/securego/gosec/v2/issue"
@@ -36,44 +37,50 @@ func containsReaderCall(node ast.Node, ctx *gosec.Context, list gosec.CallList)
if list.ContainsPkgCallExpr(node, ctx, false) != nil {
return true
}
// Resolve type info of ident (for *archive/zip.File.Open)
// Resolve type info for selector calls like file.Open()
s, idt, _ := gosec.GetCallInfo(node, ctx)
return list.Contains(s, idt)
}
func (d *decompressionBombCheck) Match(node ast.Node, ctx *gosec.Context) (*issue.Issue, error) {
var readerVarObj map[*ast.Object]struct{}
var readerVars map[*types.Var]struct{}
// To check multiple lines, ctx.PassedValues is used to store temporary data.
// Use ctx.PassedValues for stateful tracking across statements.
if _, ok := ctx.PassedValues[d.ID()]; !ok {
readerVarObj = make(map[*ast.Object]struct{})
ctx.PassedValues[d.ID()] = readerVarObj
} else if pv, ok := ctx.PassedValues[d.ID()].(map[*ast.Object]struct{}); ok {
readerVarObj = pv
readerVars = make(map[*types.Var]struct{})
ctx.PassedValues[d.ID()] = readerVars
} else if pv, ok := ctx.PassedValues[d.ID()].(map[*types.Var]struct{}); ok {
readerVars = pv
} else {
return nil, fmt.Errorf("PassedValues[%s] of Context is not map[*ast.Object]struct{}, but %T", d.ID(), ctx.PassedValues[d.ID()])
return nil, fmt.Errorf("PassedValues[%s] of Context is not map[*types.Var]struct{}, but %T", d.ID(), ctx.PassedValues[d.ID()])
}
// io.Copy is a common function.
// To reduce false positives, This rule detects code which is used for compressed data only.
switch n := node.(type) {
case *ast.AssignStmt:
for _, expr := range n.Rhs {
for i, expr := range n.Rhs {
if callExpr, ok := expr.(*ast.CallExpr); ok && containsReaderCall(callExpr, ctx, d.readerCalls) {
if idt, ok := n.Lhs[0].(*ast.Ident); ok && idt.Name != "_" {
// Example:
// r, _ := zlib.NewReader(buf)
// Add r's Obj to readerVarObj map
readerVarObj[idt.Obj] = struct{}{}
if i < len(n.Lhs) {
if idt, ok := n.Lhs[i].(*ast.Ident); ok && idt.Name != "_" {
if obj := ctx.Info.ObjectOf(idt); obj != nil {
if v, ok := obj.(*types.Var); ok {
readerVars[v] = struct{}{}
}
}
}
}
}
}
case *ast.CallExpr:
if d.copyCalls.ContainsPkgCallExpr(n, ctx, false) != nil {
if idt, ok := n.Args[1].(*ast.Ident); ok {
if _, ok := readerVarObj[idt.Obj]; ok {
// Detect io.Copy(x, r)
return ctx.NewIssue(n, d.ID(), d.What, d.Severity, d.Confidence), nil
if len(n.Args) > 1 {
if idt, ok := n.Args[1].(*ast.Ident); ok {
if obj := ctx.Info.ObjectOf(idt); obj != nil {
if v, ok := obj.(*types.Var); ok {
if _, tracked := readerVars[v]; tracked {
return ctx.NewIssue(n, d.ID(), d.What, d.Severity, d.Confidence), nil
}
}
}
}
}
}
@@ -82,7 +89,7 @@ func (d *decompressionBombCheck) Match(node ast.Node, ctx *gosec.Context) (*issu
return nil, nil
}
// NewDecompressionBombCheck detects if there is potential DoS vulnerability via decompression bomb
// NewDecompressionBombCheck detects potential DoS via decompression bomb
func NewDecompressionBombCheck(id string, _ gosec.Config) (gosec.Rule, []ast.Node) {
readerCalls := gosec.NewCallList()
readerCalls.Add("compress/gzip", "NewReader")
@@ -107,5 +114,5 @@ func NewDecompressionBombCheck(id string, _ gosec.Config) (gosec.Rule, []ast.Nod
},
readerCalls: readerCalls,
copyCalls: copyCalls,
}, []ast.Node{(*ast.FuncDecl)(nil), (*ast.AssignStmt)(nil), (*ast.CallExpr)(nil)}
}, []ast.Node{(*ast.AssignStmt)(nil), (*ast.CallExpr)(nil)}
}

View File

@@ -11,7 +11,7 @@ import (
type implicitAliasing struct {
issue.MetaData
aliases map[*ast.Object]struct{}
aliases map[*types.Var]struct{}
rightBrace token.Pos
acceptableAlias []*ast.UnaryExpr
}
@@ -47,68 +47,64 @@ func doGetIdentExpr(expr ast.Expr, hasSelector bool) (*ast.Ident, bool) {
}
func (r *implicitAliasing) Match(n ast.Node, c *gosec.Context) (*issue.Issue, error) {
// This rule does not apply for Go 1.22, see https://go.dev/doc/go1.22#language.
// This rule does not apply for Go 1.22+, where range loop variables have per-iteration scope.
// See https://go.dev/doc/go1.22#language.
major, minor, _ := gosec.GoVersion()
if major >= 1 && minor >= 22 {
if major == 1 && minor >= 22 || major > 1 {
return nil, nil
}
switch node := n.(type) {
case *ast.RangeStmt:
// When presented with a range statement, get the underlying Object bound to
// by assignment and add it to our set (r.aliases) of objects to check for.
if key, ok := node.Value.(*ast.Ident); ok {
if key.Obj != nil {
if assignment, ok := key.Obj.Decl.(*ast.AssignStmt); ok {
if len(assignment.Lhs) < 2 {
return nil, nil
}
if object, ok := assignment.Lhs[1].(*ast.Ident); ok {
r.aliases[object.Obj] = struct{}{}
if r.rightBrace < node.Body.Rbrace {
r.rightBrace = node.Body.Rbrace
}
// Add the range value variable (if it's an identifier) to the set of aliased loop vars.
if valueIdent, ok := node.Value.(*ast.Ident); ok {
if obj := c.Info.ObjectOf(valueIdent); obj != nil {
if v, ok := obj.(*types.Var); ok {
r.aliases[v] = struct{}{}
if r.rightBrace < node.Body.Rbrace {
r.rightBrace = node.Body.Rbrace
}
}
}
}
case *ast.UnaryExpr:
// If this unary expression is outside of the last range statement we were looking at
// then clear the list of objects we're concerned about because they're no longer in
// scope
// Clear aliases if we're outside the last tracked range loop body.
if node.Pos() > r.rightBrace {
r.aliases = make(map[*ast.Object]struct{})
r.aliases = make(map[*types.Var]struct{})
r.acceptableAlias = make([]*ast.UnaryExpr, 0)
}
// Short circuit logic to skip checking aliases if we have nothing to check against.
// Short-circuit if no aliases to check.
if len(r.aliases) == 0 {
return nil, nil
}
// If this unary is at the top level of a return statement then it is okay--
// see *ast.ReturnStmt comment below.
// Acceptable if this &expr is directly returned (top-level in return stmt).
if containsUnary(r.acceptableAlias, node) {
return nil, nil
}
// If we find a unary op of & (reference) of an object within r.aliases, complain.
if identExpr, hasSelector := getIdentExpr(node); identExpr != nil && node.Op.String() == "&" {
if _, contains := r.aliases[identExpr.Obj]; contains {
_, isPointer := c.Info.TypeOf(identExpr).(*types.Pointer)
if !hasSelector || !isPointer {
return c.NewIssue(n, r.ID(), r.What, r.Severity, r.Confidence), nil
// Check for & on a tracked loop variable.
if node.Op == token.AND {
if identExpr, hasSelector := getIdentExpr(node.X); identExpr != nil {
if obj := c.Info.ObjectOf(identExpr); obj != nil {
if v, ok := obj.(*types.Var); ok {
if _, aliased := r.aliases[v]; aliased {
_, isPointer := c.Info.TypeOf(identExpr).(*types.Pointer)
if !hasSelector || !isPointer {
return c.NewIssue(n, r.ID(), r.What, r.Severity, r.Confidence), nil
}
}
}
}
}
}
case *ast.ReturnStmt:
// Returning a rangeStmt yielded value is acceptable since only one value will be returned
for _, item := range node.Results {
if unary, ok := item.(*ast.UnaryExpr); ok && unary.Op.String() == "&" {
// Mark direct &loopVar in return statements as acceptable (only one iteration's value returned).
for _, res := range node.Results {
if unary, ok := res.(*ast.UnaryExpr); ok && unary.Op == token.AND {
r.acceptableAlias = append(r.acceptableAlias, unary)
}
}
@@ -117,10 +113,10 @@ func (r *implicitAliasing) Match(n ast.Node, c *gosec.Context) (*issue.Issue, er
return nil, nil
}
// NewImplicitAliasing detects implicit memory aliasing of type: for blah := SomeCall() {... SomeOtherCall(&blah) ...}
// NewImplicitAliasing detects implicit memory aliasing in range loops (pre-Go 1.22).
func NewImplicitAliasing(id string, _ gosec.Config) (gosec.Rule, []ast.Node) {
return &implicitAliasing{
aliases: make(map[*ast.Object]struct{}),
aliases: make(map[*types.Var]struct{}),
rightBrace: token.NoPos,
acceptableAlias: make([]*ast.UnaryExpr, 0),
MetaData: issue.MetaData{

View File

@@ -17,6 +17,7 @@ package rules
import (
"fmt"
"go/ast"
"go/types"
"github.com/securego/gosec/v2"
"github.com/securego/gosec/v2/issue"
@@ -32,39 +33,45 @@ func (i *integerOverflowCheck) ID() string {
}
func (i *integerOverflowCheck) Match(node ast.Node, ctx *gosec.Context) (*issue.Issue, error) {
var atoiVarObj map[*ast.Object]ast.Node
var atoiVars map[*types.Var]struct{}
// To check multiple lines, ctx.PassedValues is used to store temporary data.
// Stateful tracking via ctx.PassedValues
if _, ok := ctx.PassedValues[i.ID()]; !ok {
atoiVarObj = make(map[*ast.Object]ast.Node)
ctx.PassedValues[i.ID()] = atoiVarObj
} else if pv, ok := ctx.PassedValues[i.ID()].(map[*ast.Object]ast.Node); ok {
atoiVarObj = pv
atoiVars = make(map[*types.Var]struct{})
ctx.PassedValues[i.ID()] = atoiVars
} else if pv, ok := ctx.PassedValues[i.ID()].(map[*types.Var]struct{}); ok {
atoiVars = pv
} else {
return nil, fmt.Errorf("PassedValues[%s] of Context is not map[*ast.Object]ast.Node, but %T", i.ID(), ctx.PassedValues[i.ID()])
return nil, fmt.Errorf("PassedValues[%s] of Context is not map[*types.Var]struct{}, but %T", i.ID(), ctx.PassedValues[i.ID()])
}
// strconv.Atoi is a common function.
// To reduce false positives, This rule detects code which is converted to int32/int16 only.
switch n := node.(type) {
case *ast.AssignStmt:
for _, expr := range n.Rhs {
if callExpr, ok := expr.(*ast.CallExpr); ok && i.calls.ContainsPkgCallExpr(callExpr, ctx, false) != nil {
if idt, ok := n.Lhs[0].(*ast.Ident); ok && idt.Name != "_" {
// Example:
// v, _ := strconv.Atoi("1111")
// Add v's Obj to atoiVarObj map
atoiVarObj[idt.Obj] = n
if len(n.Lhs) > 0 {
if idt, ok := n.Lhs[0].(*ast.Ident); ok && idt.Name != "_" {
if obj := ctx.Info.ObjectOf(idt); obj != nil {
if v, ok := obj.(*types.Var); ok {
atoiVars[v] = struct{}{}
}
}
}
}
}
}
case *ast.CallExpr:
if fun, ok := n.Fun.(*ast.Ident); ok {
if fun.Name == "int32" || fun.Name == "int16" {
if idt, ok := n.Args[0].(*ast.Ident); ok {
if _, ok := atoiVarObj[idt.Obj]; ok {
// Detect int32(v) and int16(v)
return ctx.NewIssue(n, i.ID(), i.What, i.Severity, i.Confidence), nil
if len(n.Args) > 0 {
if idt, ok := n.Args[0].(*ast.Ident); ok {
if obj := ctx.Info.ObjectOf(idt); obj != nil {
if v, ok := obj.(*types.Var); ok {
if _, tracked := atoiVars[v]; tracked {
return ctx.NewIssue(n, i.ID(), i.What, i.Severity, i.Confidence), nil
}
}
}
}
}
}
@@ -74,7 +81,7 @@ func (i *integerOverflowCheck) Match(node ast.Node, ctx *gosec.Context) (*issue.
return nil, nil
}
// NewIntegerOverflowCheck detects if there is potential Integer OverFlow
// NewIntegerOverflowCheck detects potential integer overflow from strconv.Atoi conversion to int16/int32
func NewIntegerOverflowCheck(id string, _ gosec.Config) (gosec.Rule, []ast.Node) {
calls := gosec.NewCallList()
calls.Add("strconv", "Atoi")
@@ -86,5 +93,5 @@ func NewIntegerOverflowCheck(id string, _ gosec.Config) (gosec.Rule, []ast.Node)
What: "Potential Integer overflow made by strconv.Atoi result conversion to int16/32",
},
calls: calls,
}, []ast.Node{(*ast.FuncDecl)(nil), (*ast.AssignStmt)(nil), (*ast.CallExpr)(nil)}
}, []ast.Node{(*ast.AssignStmt)(nil), (*ast.CallExpr)(nil)}
}

View File

@@ -27,10 +27,11 @@ type readfile struct {
gosec.CallList
pathJoin gosec.CallList
clean gosec.CallList
// cleanedVar maps the declaration node of an identifier to the Clean() call node
cleanedVar map[any]ast.Node
// joinedVar maps the declaration node of an identifier to the Join() call node
joinedVar map[any]ast.Node
// cleanedVar maps the defining *types.Var (result of Clean) to the Clean call node
cleanedVar map[*types.Var]ast.Node
// joinedVar maps the defining *types.Var (result of Join) to the Join call node
joinedVar map[*types.Var]ast.Node
}
// ID returns the identifier for this rule
@@ -38,23 +39,21 @@ func (r *readfile) ID() string {
return r.MetaData.ID
}
// isJoinFunc checks if there is a filepath.Join or other join function
// isJoinFunc checks if the call is a filepath.Join with at least one non-constant argument
func (r *readfile) isJoinFunc(n ast.Node, c *gosec.Context) bool {
if call := r.pathJoin.ContainsPkgCallExpr(n, c, false); call != nil {
for _, arg := range call.Args {
// edge case: check if one of the args is a BinaryExpr
if binExp, ok := arg.(*ast.BinaryExpr); ok {
// iterate and resolve all found identities from the BinaryExpr
if _, ok := gosec.FindVarIdentities(binExp, c); ok {
return true
}
}
// try and resolve identity
if ident, ok := arg.(*ast.Ident); ok {
obj := c.Info.ObjectOf(ident)
if _, ok := obj.(*types.Var); ok && !gosec.TryResolve(ident, c) {
return true
if obj := c.Info.ObjectOf(ident); obj != nil {
if _, ok := obj.(*types.Var); ok && !gosec.TryResolve(ident, c) {
return true
}
}
}
}
@@ -62,147 +61,118 @@ func (r *readfile) isJoinFunc(n ast.Node, c *gosec.Context) bool {
return false
}
// isFilepathClean checks if there is a filepath.Clean for given variable
func (r *readfile) isFilepathClean(n *ast.Ident, c *gosec.Context) bool {
// quick lookup: was this var's declaration recorded as a Clean() call?
if _, ok := r.cleanedVar[n.Obj.Decl]; ok {
return true
}
if n.Obj.Kind != ast.Var {
return false
}
if node, ok := n.Obj.Decl.(*ast.AssignStmt); ok {
if call, ok := node.Rhs[0].(*ast.CallExpr); ok {
if clean := r.clean.ContainsPkgCallExpr(call, c, false); clean != nil {
return true
}
}
}
return false
// isFilepathClean checks if the variable is the result of a filepath.Clean (or similar) call
func (r *readfile) isFilepathClean(v *types.Var, _ *gosec.Context) bool {
_, ok := r.cleanedVar[v]
return ok
}
// trackFilepathClean tracks back the declaration of variable from filepath.Clean argument
func (r *readfile) trackFilepathClean(n ast.Node) {
if clean, ok := n.(*ast.CallExpr); ok && len(clean.Args) > 0 {
if ident, ok := clean.Args[0].(*ast.Ident); ok {
// ident.Obj may be nil if the referenced declaration is in another file. It also may be incorrect.
// if it is nil, do not follow it.
if ident.Obj != nil {
r.cleanedVar[ident.Obj.Decl] = n
}
}
}
}
// trackJoinAssignStmt tracks assignments where RHS is a Join(...) call and LHS is an identifier
func (r *readfile) trackJoinAssignStmt(node *ast.AssignStmt, c *gosec.Context) {
if len(node.Rhs) == 0 {
// trackCleanAssign records a variable defined as the result of a Clean() call
func (r *readfile) trackCleanAssign(assign *ast.AssignStmt, c *gosec.Context) {
if len(assign.Rhs) == 0 {
return
}
if call, ok := node.Rhs[0].(*ast.CallExpr); ok {
if r.pathJoin.ContainsPkgCallExpr(call, c, false) != nil {
// LHS must be an identifier (simple case)
if len(node.Lhs) > 0 {
if ident, ok := node.Lhs[0].(*ast.Ident); ok && ident.Obj != nil {
r.joinedVar[ident.Obj.Decl] = call
if cleanCall, ok := assign.Rhs[0].(*ast.CallExpr); ok {
if r.clean.ContainsPkgCallExpr(cleanCall, c, false) != nil {
if len(assign.Lhs) > 0 {
if ident, ok := assign.Lhs[0].(*ast.Ident); ok {
if obj := c.Info.ObjectOf(ident); obj != nil {
if v, ok := obj.(*types.Var); ok {
r.cleanedVar[v] = cleanCall
}
}
}
}
}
}
}
// osRootSuggestion returns an Autofix suggesting the use of os.Root where supported
// to constrain file access under a fixed directory and mitigate traversal risks.
// trackJoinAssignStmt records a variable defined from a Join() call
func (r *readfile) trackJoinAssignStmt(assign *ast.AssignStmt, c *gosec.Context) {
if len(assign.Rhs) == 0 {
return
}
if call, ok := assign.Rhs[0].(*ast.CallExpr); ok {
if r.pathJoin.ContainsPkgCallExpr(call, c, false) != nil {
if len(assign.Lhs) > 0 {
if ident, ok := assign.Lhs[0].(*ast.Ident); ok {
if obj := c.Info.ObjectOf(ident); obj != nil {
if v, ok := obj.(*types.Var); ok {
r.joinedVar[v] = call
}
}
}
}
}
}
}
// osRootSuggestion returns an Autofix suggestion for os.Root (Go 1.24+)
func (r *readfile) osRootSuggestion() string {
major, minor, _ := gosec.GoVersion()
if major == 1 && minor >= 24 {
if major == 1 && minor >= 24 || major > 1 {
return "Consider using os.Root to scope file access under a fixed root (Go >=1.24). Prefer root.Open/root.Stat over os.Open/os.Stat to prevent directory traversal."
}
return ""
}
// isSafeJoin checks if path is baseDir + filepath.Clean(fn) joined.
// improvements over earlier naive version:
// - allow baseDir as a BasicLit or as an identifier that resolves to a string constant
// - accept Clean(...) being either a CallExpr or an identifier previously recorded as Clean result
// isSafeJoin checks for safe Join(baseConstant, cleanedOrConstant)
func (r *readfile) isSafeJoin(call *ast.CallExpr, c *gosec.Context) bool {
join := r.pathJoin.ContainsPkgCallExpr(call, c, false)
if join == nil {
if r.pathJoin.ContainsPkgCallExpr(call, c, false) == nil {
return false
}
// We expect join.Args to include a baseDir-like arg and a cleaned path arg.
var foundBaseDir bool
var foundCleanArg bool
var hasBaseDir bool
var hasCleanArg bool
for _, arg := range join.Args {
for _, arg := range call.Args {
switch a := arg.(type) {
case *ast.BasicLit:
// literal string or similar — treat as possible baseDir
foundBaseDir = true
hasBaseDir = true
case *ast.Ident:
// If ident is resolvable to a constant string (TryResolve true), treat as baseDir.
// Or if ident refers to a variable that was itself assigned from a constant BasicLit,
// it's considered safe as baseDir.
if gosec.TryResolve(a, c) {
foundBaseDir = true
} else {
// It might be a cleaned variable: e.g. cleanPath := filepath.Clean(fn)
if r.isFilepathClean(a, c) {
foundCleanArg = true
hasBaseDir = true
} else if obj := c.Info.ObjectOf(a); obj != nil {
if v, ok := obj.(*types.Var); ok && r.isFilepathClean(v, c) {
hasCleanArg = true
}
}
case *ast.CallExpr:
// If an argument is a Clean() call directly, mark clean arg found.
if r.clean.ContainsPkgCallExpr(a, c, false) != nil {
foundCleanArg = true
hasCleanArg = true
}
default:
// ignore other types
}
}
return foundBaseDir && foundCleanArg
return hasBaseDir && hasCleanArg
}
func (r *readfile) Match(n ast.Node, c *gosec.Context) (*issue.Issue, error) {
// Track filepath.Clean usages so identifiers assigned from Clean() are known.
if node := r.clean.ContainsPkgCallExpr(n, c, false); node != nil {
r.trackFilepathClean(n)
return nil, nil
}
// Track Join assignments if we see an AssignStmt whose RHS is a Join call.
// Track assignments from Clean() or Join()
if assign, ok := n.(*ast.AssignStmt); ok {
// track join result assigned to a variable, e.g., fullPath := filepath.Join(baseDir, cleanPath)
r.trackCleanAssign(assign, c)
r.trackJoinAssignStmt(assign, c)
// also track Clean assignment if present on RHS
if len(assign.Rhs) > 0 {
if call, ok := assign.Rhs[0].(*ast.CallExpr); ok {
if r.clean.ContainsPkgCallExpr(call, c, false) != nil {
r.trackFilepathClean(call)
}
}
}
// continue, don't return here — other checks may apply
}
// Now check for file-reading calls (os.Open, os.OpenFile, ioutil.ReadFile etc.)
if node := r.ContainsPkgCallExpr(n, c, false); node != nil {
if len(node.Args) == 0 {
// Main check: file reading calls
if readCall := r.ContainsPkgCallExpr(n, c, false); readCall != nil {
if len(readCall.Args) == 0 {
return nil, nil
}
arg := node.Args[0]
pathArg := readCall.Args[0]
// If argument is a call expression, check for Join/Clean patterns.
if callExpr, ok := arg.(*ast.CallExpr); ok {
// If this call matches a safe Join(baseDir, Clean(...)) pattern, treat as safe.
if r.isSafeJoin(callExpr, c) {
// safe pattern detected; do not raise an issue
// Direct Clean() call as argument → safe
if cleanCall, ok := pathArg.(*ast.CallExpr); ok {
if r.clean.ContainsPkgCallExpr(cleanCall, c, false) != nil {
return nil, nil
}
// If the argument is a Join call but not safe per above, flag it (as before)
if r.isJoinFunc(callExpr, c) {
}
// Direct Join() call as argument
if joinCall, ok := pathArg.(*ast.CallExpr); ok {
if r.isSafeJoin(joinCall, c) {
return nil, nil
}
if r.isJoinFunc(joinCall, c) {
iss := c.NewIssue(n, r.ID(), r.What, r.Severity, r.Confidence)
if s := r.osRootSuggestion(); s != "" {
iss.Autofix = s
@@ -211,20 +181,17 @@ func (r *readfile) Match(n ast.Node, c *gosec.Context) (*issue.Issue, error) {
}
}
// If arg is an identifier that was assigned from a Join(...) call, check that recorded Join call.
if ident, ok := arg.(*ast.Ident); ok {
if ident.Obj != nil {
if joinCall, ok := r.joinedVar[ident.Obj.Decl]; ok {
// If the identifier itself was later cleaned, treat as safe regardless of original Join args
if r.isFilepathClean(ident, c) {
return nil, nil
}
// joinCall is a *ast.CallExpr; check if that join is a safe join
if jc, ok := joinCall.(*ast.CallExpr); ok {
if r.isSafeJoin(jc, c) {
// Variable assigned from Join()
if ident, ok := pathArg.(*ast.Ident); ok {
if obj := c.Info.ObjectOf(ident); obj != nil {
if v, ok := obj.(*types.Var); ok {
if joinCall, ok := r.joinedVar[v]; ok {
if r.isFilepathClean(v, c) {
return nil, nil
}
if jc, ok := joinCall.(*ast.CallExpr); ok && r.isSafeJoin(jc, c) {
return nil, nil
}
// join exists but is not safe: flag it
iss := c.NewIssue(n, r.ID(), r.What, r.Severity, r.Confidence)
if s := r.osRootSuggestion(); s != "" {
iss.Autofix = s
@@ -235,9 +202,8 @@ func (r *readfile) Match(n ast.Node, c *gosec.Context) (*issue.Issue, error) {
}
}
// handles binary string concatenation eg. ioutil.Readfile("/tmp/" + file + "/blob")
if binExp, ok := arg.(*ast.BinaryExpr); ok {
// resolve all found identities from the BinaryExpr
// Binary concatenation
if binExp, ok := pathArg.(*ast.BinaryExpr); ok {
if _, ok := gosec.FindVarIdentities(binExp, c); ok {
iss := c.NewIssue(n, r.ID(), r.What, r.Severity, r.Confidence)
if s := r.osRootSuggestion(); s != "" {
@@ -247,37 +213,39 @@ func (r *readfile) Match(n ast.Node, c *gosec.Context) (*issue.Issue, error) {
}
}
// if it's a plain identifier, and not resolved and not cleaned, flag it
if ident, ok := arg.(*ast.Ident); ok {
obj := c.Info.ObjectOf(ident)
if _, ok := obj.(*types.Var); ok &&
!gosec.TryResolve(ident, c) &&
!r.isFilepathClean(ident, c) {
iss := c.NewIssue(n, r.ID(), r.What, r.Severity, r.Confidence)
if s := r.osRootSuggestion(); s != "" {
iss.Autofix = s
// Plain variable — tainted unless constant or cleaned
if ident, ok := pathArg.(*ast.Ident); ok {
if obj := c.Info.ObjectOf(ident); obj != nil {
if v, ok := obj.(*types.Var); ok {
if gosec.TryResolve(ident, c) || r.isFilepathClean(v, c) {
return nil, nil
}
iss := c.NewIssue(n, r.ID(), r.What, r.Severity, r.Confidence)
if s := r.osRootSuggestion(); s != "" {
iss.Autofix = s
}
return iss, nil
}
return iss, nil
}
}
}
return nil, nil
}
// NewReadFile detects cases where we read files
// NewReadFile detects potential file inclusion via variable in file read operations
func NewReadFile(id string, _ gosec.Config) (gosec.Rule, []ast.Node) {
rule := &readfile{
pathJoin: gosec.NewCallList(),
clean: gosec.NewCallList(),
CallList: gosec.NewCallList(),
pathJoin: gosec.NewCallList(),
clean: gosec.NewCallList(),
CallList: gosec.NewCallList(),
cleanedVar: make(map[*types.Var]ast.Node),
joinedVar: make(map[*types.Var]ast.Node),
MetaData: issue.MetaData{
ID: id,
What: "Potential file inclusion via variable",
Severity: issue.Medium,
Confidence: issue.High,
},
cleanedVar: map[any]ast.Node{},
joinedVar: map[any]ast.Node{},
}
rule.pathJoin.Add("path/filepath", "Join")
rule.pathJoin.Add("path", "Join")

View File

@@ -17,6 +17,8 @@ package rules
import (
"fmt"
"go/ast"
"go/token"
"go/types"
"regexp"
"github.com/securego/gosec/v2"
@@ -60,33 +62,27 @@ var sqlCallIdents = map[string]map[string]int{
},
}
// findQueryArg locates the argument taking raw SQL
// findQueryArg locates the argument taking raw SQL.
func findQueryArg(call *ast.CallExpr, ctx *gosec.Context) (ast.Expr, error) {
typeName, fnName, err := gosec.GetCallInfo(call, ctx)
if err != nil {
return nil, err
}
i := -1
if ni, ok := sqlCallIdents[typeName]; ok {
if i, ok = ni[fnName]; !ok {
i = -1
if methods, ok := sqlCallIdents[typeName]; ok {
if i, ok := methods[fnName]; ok && i < len(call.Args) {
return call.Args[i], nil
}
}
if i == -1 {
return nil, fmt.Errorf("SQL argument index not found for %s.%s", typeName, fnName)
}
if i >= len(call.Args) {
return nil, nil
}
query := call.Args[i]
return query, nil
return nil, fmt.Errorf("SQL argument index not found for %s.%s", typeName, fnName)
}
func (s *sqlStatement) ID() string {
return s.MetaData.ID
}
// See if the string matches the patterns for the statement.
// MatchPatterns checks if the string matches all required SQL patterns.
func (s *sqlStatement) MatchPatterns(str string) bool {
for _, pattern := range s.patterns {
if !pattern.MatchString(str) {
@@ -104,8 +100,9 @@ func (s *sqlStrConcat) ID() string {
return s.MetaData.ID
}
// findInjectionInBranch walks diwb a set if expressions, and will create new issues if it finds SQL injections
// This method assumes you've already verified that the branch contains SQL syntax
// findInjectionInBranch walks through a set of expressions and returns the first
// binary expression containing a potential injection (non-constant operand).
// This method assumes the branch already contains SQL syntax.
func (s *sqlStrConcat) findInjectionInBranch(ctx *gosec.Context, branch []ast.Expr) *ast.BinaryExpr {
for _, node := range branch {
be, ok := node.(*ast.BinaryExpr)
@@ -113,114 +110,194 @@ func (s *sqlStrConcat) findInjectionInBranch(ctx *gosec.Context, branch []ast.Ex
continue
}
operands := gosec.GetBinaryExprOperands(be)
for _, op := range operands {
if _, ok := op.(*ast.BasicLit); ok {
for _, op := range gosec.GetBinaryExprOperands(be) {
if gosec.TryResolve(op, ctx) {
continue
}
if ident, ok := op.(*ast.Ident); ok && s.checkObject(ident, ctx) {
continue
}
return be
}
}
return nil
}
// see if we can figure out what it is
func (s *sqlStrConcat) checkObject(n *ast.Ident, c *gosec.Context) bool {
if n.Obj != nil {
return n.Obj.Kind != ast.Var && n.Obj.Kind != ast.Fun
}
// Try to resolve unresolved identifiers using other files in same package
for _, file := range c.PkgFiles {
if node, ok := file.Scope.Objects[n.String()]; ok {
return node.Kind != ast.Var && node.Kind != ast.Fun
}
}
return false
}
// checkQuery verifies if the query parameters is a string concatenation
// checkQuery verifies if the query parameter involves risky string concatenation.
func (s *sqlStrConcat) checkQuery(call *ast.CallExpr, ctx *gosec.Context) (*issue.Issue, error) {
query, err := findQueryArg(call, ctx)
if err != nil {
return nil, err
}
// Direct binary concatenation (e.g., "SELECT ..." + tainted)
if be, ok := query.(*ast.BinaryExpr); ok {
operands := gosec.GetBinaryExprOperands(be)
if start, ok := operands[0].(*ast.BasicLit); ok {
if str, e := gosec.GetString(start); e == nil {
if !s.MatchPatterns(str) {
return nil, nil
if str, e := gosec.GetString(start); e == nil && s.MatchPatterns(str) {
for _, op := range operands[1:] {
if gosec.TryResolve(op, ctx) {
continue
}
return ctx.NewIssue(be, s.ID(), s.What, s.Severity, s.Confidence), nil
}
}
for _, op := range operands[1:] {
if _, ok := op.(*ast.BasicLit); ok {
continue
}
return nil, nil
}
// Must be an identifier to continue (e.g., var query = ...; query += ...)
ident, ok := query.(*ast.Ident)
if !ok {
return nil, nil
}
v, ok := ctx.Info.ObjectOf(ident).(*types.Var)
if !ok {
return nil, nil
}
// Determine search scope (package-level or local)
isPkgLevel := ctx.Pkg != nil && v.Parent() == ctx.Pkg.Scope()
var filesToSearch []*ast.File
if isPkgLevel {
filesToSearch = ctx.PkgFiles
} else {
callFile := gosec.ContainingFile(call, ctx)
if callFile == nil {
return nil, nil
}
filesToSearch = []*ast.File{callFile}
}
// Find the defining declaration and check for SQL patterns / initial risky concatenation
declRHS := []ast.Expr{}
foundDecl := false
// Determine the file containing the variable's defining position
var declFile *ast.File
if ctx.FileSet != nil {
if posFile := ctx.FileSet.File(v.Pos()); posFile != nil {
targetName := posFile.Name()
for _, f := range filesToSearch {
if fileInfo := ctx.FileSet.File(f.Pos()); fileInfo != nil && fileInfo.Name() == targetName {
declFile = f
break
}
if op, ok := op.(*ast.Ident); ok && s.checkObject(op, ctx) {
continue
}
return ctx.NewIssue(be, s.ID(), s.What, s.Severity, s.Confidence), nil
}
}
}
// Handle the case where an injection occurs as an infixed string concatenation, ie "SELECT * FROM foo WHERE name = '" + os.Args[0] + "' AND 1=1"
if id, ok := query.(*ast.Ident); ok {
var match bool
for _, str := range gosec.GetIdentStringValuesRecursive(id) {
if s.MatchPatterns(str) {
match = true
if declFile != nil {
ast.Inspect(declFile, func(n ast.Node) bool {
switch d := n.(type) {
case *ast.ValueSpec:
for _, name := range d.Names {
if name.Pos() == v.Pos() && ctx.Info.ObjectOf(name) == v {
declRHS = d.Values
foundDecl = true
return false // Stop inspection
}
}
case *ast.AssignStmt:
if d.Tok == token.DEFINE { // Only short variable declarations define new vars
for _, lhs := range d.Lhs {
if id, ok := lhs.(*ast.Ident); ok && id.Pos() == v.Pos() && ctx.Info.ObjectOf(id) == v {
declRHS = d.Rhs
foundDecl = true
return false // Stop inspection
}
}
}
}
return true
})
}
if foundDecl {
// Check for SQL patterns in initial values
hasSQLPattern := false
for _, val := range declRHS {
if str, err := gosec.GetStringRecursive(val); err == nil && s.MatchPatterns(str) {
hasSQLPattern = true
break
}
}
if !match {
return nil, nil
// Check for risky initial concatenation
if inj := s.findInjectionInBranch(ctx, declRHS); inj != nil {
return ctx.NewIssue(inj, s.ID(), s.What, s.Severity, s.Confidence), nil
}
switch decl := id.Obj.Decl.(type) {
case *ast.AssignStmt:
if injection := s.findInjectionInBranch(ctx, decl.Rhs); injection != nil {
return ctx.NewIssue(injection, s.ID(), s.What, s.Severity, s.Confidence), nil
if !hasSQLPattern {
return nil, nil
}
} else {
// No defining declaration found → assume not SQL-related
return nil, nil
}
// Check for risky mutations (query += tainted or query = query + tainted)
for _, f := range filesToSearch {
var found *ast.AssignStmt
ast.Inspect(f, func(n ast.Node) bool {
assign, ok := n.(*ast.AssignStmt)
if !ok || len(assign.Lhs) != 1 || len(assign.Rhs) != 1 {
return true
}
case *ast.ValueSpec:
// handle: var query string = "SELECT ...'" + user
if injection := s.findInjectionInBranch(ctx, decl.Values); injection != nil {
return ctx.NewIssue(injection, s.ID(), s.What, s.Severity, s.Confidence), nil
lIdent, ok := assign.Lhs[0].(*ast.Ident)
if !ok || ctx.Info.ObjectOf(lIdent) != v {
return true
}
var appended ast.Expr
switch assign.Tok {
case token.ADD_ASSIGN:
appended = assign.Rhs[0]
case token.ASSIGN:
be, ok := assign.Rhs[0].(*ast.BinaryExpr)
if !ok || be.Op != token.ADD {
return true
}
left, ok := be.X.(*ast.Ident)
if !ok || ctx.Info.ObjectOf(left) != v {
return true
}
appended = be.Y
default:
return true
}
if !gosec.TryResolve(appended, ctx) {
found = assign
return false
}
return true
})
if found != nil {
return ctx.NewIssue(found, s.ID(), s.What, s.Severity, s.Confidence), nil
}
}
return nil, nil
}
// Checks SQL query concatenation issues such as "SELECT * FROM table WHERE " + " ' OR 1=1"
// Match looks for SQL execution calls and checks for concatenation issues.
func (s *sqlStrConcat) Match(n ast.Node, ctx *gosec.Context) (*issue.Issue, error) {
switch stmt := n.(type) {
case *ast.AssignStmt:
for _, expr := range stmt.Rhs {
if sqlQueryCall, ok := expr.(*ast.CallExpr); ok && s.ContainsCallExpr(expr, ctx) != nil {
return s.checkQuery(sqlQueryCall, ctx)
if call, ok := expr.(*ast.CallExpr); ok && s.ContainsCallExpr(expr, ctx) != nil {
return s.checkQuery(call, ctx)
}
}
case *ast.ExprStmt:
if sqlQueryCall, ok := stmt.X.(*ast.CallExpr); ok && s.ContainsCallExpr(stmt.X, ctx) != nil {
return s.checkQuery(sqlQueryCall, ctx)
if call, ok := stmt.X.(*ast.CallExpr); ok && s.ContainsCallExpr(call, ctx) != nil {
return s.checkQuery(call, ctx)
}
}
return nil, nil
}
// NewSQLStrConcat looks for cases where we are building SQL strings via concatenation
// NewSQLStrConcat creates a rule for detecting SQL string concatenation.
func NewSQLStrConcat(id string, _ gosec.Config) (gosec.Rule, []ast.Node) {
rule := &sqlStrConcat{
sqlStatement: sqlStatement{
@@ -237,9 +314,9 @@ func NewSQLStrConcat(id string, _ gosec.Config) (gosec.Rule, []ast.Node) {
},
}
for s, si := range sqlCallIdents {
for i := range si {
rule.Add(s, i)
for typ, methods := range sqlCallIdents {
for method := range methods {
rule.Add(typ, method)
}
}
return rule, []ast.Node{(*ast.AssignStmt)(nil), (*ast.ExprStmt)(nil)}
@@ -253,65 +330,77 @@ type sqlStrFormat struct {
noIssueQuoted gosec.CallList
}
// see if we can figure out what it is
func (s *sqlStrFormat) constObject(e ast.Expr, c *gosec.Context) bool {
n, ok := e.(*ast.Ident)
if !ok {
return false
}
if n.Obj != nil {
return n.Obj.Kind == ast.Con
}
// Try to resolve unresolved identifiers using other files in same package
for _, file := range c.PkgFiles {
if node, ok := file.Scope.Objects[n.String()]; ok {
return node.Kind == ast.Con
}
}
return false
}
// checkQuery verifies if the query parameter involves risky formatting.
func (s *sqlStrFormat) checkQuery(call *ast.CallExpr, ctx *gosec.Context) (*issue.Issue, error) {
query, err := findQueryArg(call, ctx)
if err != nil {
return nil, err
}
if ident, ok := query.(*ast.Ident); ok && ident.Obj != nil {
decl := ident.Obj.Decl
if assign, ok := decl.(*ast.AssignStmt); ok {
for _, expr := range assign.Rhs {
issue := s.checkFormatting(expr, ctx)
if issue != nil {
return issue, err
}
}
}
// Must be a variable identifier (short-declared with :=)
ident, ok := query.(*ast.Ident)
if !ok {
return nil, nil
}
return nil, nil
v, ok := ctx.Info.ObjectOf(ident).(*types.Var)
if !ok {
return nil, nil
}
// Short variable declarations are always local → use the file containing the call
callFile := gosec.ContainingFile(call, ctx)
if callFile == nil {
return nil, nil
}
// Find the defining short declaration (query := fmt.Sprintf(...))
var foundIssue *issue.Issue
ast.Inspect(callFile, func(n ast.Node) bool {
assign, ok := n.(*ast.AssignStmt)
if !ok || assign.Tok != token.DEFINE {
return true
}
// Find the LHS identifier that defines this variable
for _, lhs := range assign.Lhs {
if defIdent, ok := lhs.(*ast.Ident); ok &&
defIdent.Pos() == v.Pos() && ctx.Info.ObjectOf(defIdent) == v {
// Check every initializer expression on the RHS
for _, expr := range assign.Rhs {
if expr == nil {
continue
}
if iss := s.checkFormatting(expr, ctx); iss != nil {
foundIssue = iss
return false // Stop entire inspection
}
}
return false // Declaration found and processed
}
}
return true
})
return foundIssue, nil
}
// checkFormatting checks if a formatting call builds a risky SQL query.
func (s *sqlStrFormat) checkFormatting(n ast.Node, ctx *gosec.Context) *issue.Issue {
// argIndex changes the function argument which gets matched to the regex
argIndex := 0
if node := s.fmtCalls.ContainsPkgCallExpr(n, ctx, false); node != nil {
// if the function is fmt.Fprintf, search for SQL statement in Args[1] instead
if sel, ok := node.Fun.(*ast.SelectorExpr); ok {
if sel.Sel.Name == "Fprintf" {
// if os.Stderr or os.Stdout is in Arg[0], mark as no issue
if arg, ok := node.Args[0].(*ast.SelectorExpr); ok {
if ident, ok := arg.X.(*ast.Ident); ok {
if s.noIssue.Contains(ident.Name, arg.Sel.Name) {
return nil
}
}
if sel, ok := node.Fun.(*ast.SelectorExpr); ok && sel.Sel.Name == "Fprintf" {
// if os.Stderr or os.Stdout is in Arg[0], mark as no issue
if arg, ok := node.Args[0].(*ast.SelectorExpr); ok {
if ident, ok := arg.X.(*ast.Ident); ok && s.noIssue.Contains(ident.Name, arg.Sel.Name) {
return nil
}
// the function is Fprintf so set argIndex = 1
argIndex = 1
}
// the function is Fprintf so set argIndex = 1
argIndex = 1
}
// no formatter
@@ -319,17 +408,8 @@ func (s *sqlStrFormat) checkFormatting(n ast.Node, ctx *gosec.Context) *issue.Is
return nil
}
var formatter string
// concats callexpr arg strings together if needed before regex evaluation
if argExpr, ok := node.Args[argIndex].(*ast.BinaryExpr); ok {
if fullStr, ok := gosec.ConcatString(argExpr); ok {
formatter = fullStr
}
} else if arg, e := gosec.GetString(node.Args[argIndex]); e == nil {
formatter = arg
}
if len(formatter) <= 0 {
formatter, ok := gosec.ConcatString(node.Args[argIndex], ctx)
if !ok || formatter == "" {
return nil
}
@@ -337,7 +417,7 @@ func (s *sqlStrFormat) checkFormatting(n ast.Node, ctx *gosec.Context) *issue.Is
if argIndex+1 < len(node.Args) {
allSafe := true
for _, arg := range node.Args[argIndex+1:] {
if n := s.noIssueQuoted.ContainsPkgCallExpr(arg, ctx, true); n == nil && !s.constObject(arg, ctx) {
if s.noIssueQuoted.ContainsPkgCallExpr(arg, ctx, true) == nil && !gosec.TryResolve(arg, ctx) {
allSafe = false
break
}
@@ -346,6 +426,7 @@ func (s *sqlStrFormat) checkFormatting(n ast.Node, ctx *gosec.Context) *issue.Is
return nil
}
}
if s.MatchPatterns(formatter) {
return ctx.NewIssue(n, s.ID(), s.What, s.Severity, s.Confidence)
}
@@ -353,37 +434,31 @@ func (s *sqlStrFormat) checkFormatting(n ast.Node, ctx *gosec.Context) *issue.Is
return nil
}
// Check SQL query formatting issues such as "fmt.Sprintf("SELECT * FROM foo where '%s', userInput)"
// Match looks for SQL calls involving formatted strings.
func (s *sqlStrFormat) Match(n ast.Node, ctx *gosec.Context) (*issue.Issue, error) {
switch stmt := n.(type) {
case *ast.AssignStmt:
for _, expr := range stmt.Rhs {
if call, ok := expr.(*ast.CallExpr); ok {
selector, ok := call.Fun.(*ast.SelectorExpr)
if !ok {
continue
}
sqlQueryCall, ok := selector.X.(*ast.CallExpr)
if ok && s.ContainsCallExpr(sqlQueryCall, ctx) != nil {
issue, err := s.checkQuery(sqlQueryCall, ctx)
if err == nil && issue != nil {
return issue, err
if sel, ok := call.Fun.(*ast.SelectorExpr); ok {
if sqlCall, ok := sel.X.(*ast.CallExpr); ok && s.ContainsCallExpr(sqlCall, ctx) != nil {
return s.checkQuery(sqlCall, ctx)
}
}
}
if sqlQueryCall, ok := expr.(*ast.CallExpr); ok && s.ContainsCallExpr(expr, ctx) != nil {
return s.checkQuery(sqlQueryCall, ctx)
if s.ContainsCallExpr(expr, ctx) != nil {
return s.checkQuery(call, ctx)
}
}
}
case *ast.ExprStmt:
if sqlQueryCall, ok := stmt.X.(*ast.CallExpr); ok && s.ContainsCallExpr(stmt.X, ctx) != nil {
return s.checkQuery(sqlQueryCall, ctx)
if call, ok := stmt.X.(*ast.CallExpr); ok && s.ContainsCallExpr(call, ctx) != nil {
return s.checkQuery(call, ctx)
}
}
return nil, nil
}
// NewSQLStrFormat looks for cases where we're building SQL query strings using format strings
// NewSQLStrFormat creates a rule for detecting SQL string formatting.
func NewSQLStrFormat(id string, _ gosec.Config) (gosec.Rule, []ast.Node) {
rule := &sqlStrFormat{
CallList: gosec.NewCallList(),
@@ -403,14 +478,13 @@ func NewSQLStrFormat(id string, _ gosec.Config) (gosec.Rule, []ast.Node) {
},
},
}
for s, si := range sqlCallIdents {
for i := range si {
rule.Add(s, i)
for typ, methods := range sqlCallIdents {
for method := range methods {
rule.Add(typ, method)
}
}
rule.fmtCalls.AddAll("fmt", "Sprint", "Sprintf", "Sprintln", "Fprintf")
rule.noIssue.AddAll("os", "Stdout", "Stderr")
rule.noIssueQuoted.Add("github.com/lib/pq", "QuoteIdentifier")
return rule, []ast.Node{(*ast.AssignStmt)(nil), (*ast.ExprStmt)(nil)}
}

View File

@@ -16,6 +16,7 @@ package rules
import (
"go/ast"
"go/token"
"go/types"
"github.com/securego/gosec/v2"
@@ -31,6 +32,29 @@ func (r *subprocess) ID() string {
return r.MetaData.ID
}
// getEnclosingBodyStart returns the position of the '{' for the innermost function body enclosing the given position.
// Returns token.NoPos if no enclosing body found.
func getEnclosingBodyStart(pos token.Pos, ctx *gosec.Context) token.Pos {
if ctx.Root == nil {
return token.NoPos
}
var bodyStart token.Pos
ast.Inspect(ctx.Root, func(n ast.Node) bool {
var body *ast.BlockStmt
switch f := n.(type) {
case *ast.FuncDecl:
body = f.Body
case *ast.FuncLit:
body = f.Body
}
if body != nil && body.Pos() <= pos && pos < body.End() && body.Lbrace.IsValid() {
bodyStart = body.Lbrace
}
return true
})
return bodyStart
}
// TODO(gm) The only real potential for command injection with a Go project
// is something like this:
//
@@ -46,49 +70,27 @@ func (r *subprocess) Match(n ast.Node, c *gosec.Context) (*issue.Issue, error) {
if r.isContext(n, c) {
args = args[1:]
}
for _, arg := range args {
for i, arg := range args {
if ident, ok := arg.(*ast.Ident); ok {
obj := c.Info.ObjectOf(ident)
// need to cast and check whether it is for a variable ?
_, variable := obj.(*types.Var)
// .. indeed it is a variable then processing is different than a normal
// field assignment
if variable {
// skip the check when the declaration is not available
if ident.Obj == nil {
continue
if v, ok := obj.(*types.Var); ok {
// Special case: struct fields OR function parameters/receivers used as executable name (i==0) -> skip
if i == 0 {
if v.IsField() {
continue
}
bodyStart := getEnclosingBodyStart(ident.Pos(), c)
if bodyStart != token.NoPos && obj.Pos() < bodyStart {
continue // Parameter or receiver (declared before body brace)
}
}
switch ident.Obj.Decl.(type) {
case *ast.AssignStmt:
_, assignment := ident.Obj.Decl.(*ast.AssignStmt)
if variable && assignment {
if !gosec.TryResolve(ident, c) {
return c.NewIssue(n, r.ID(), "Subprocess launched with variable", issue.Medium, issue.High), nil
}
}
case *ast.Field:
_, field := ident.Obj.Decl.(*ast.Field)
if variable && field {
// check if the variable exist in the scope
vv, vvok := obj.(*types.Var)
if vvok && vv.Parent().Lookup(ident.Name) == nil {
return c.NewIssue(n, r.ID(), "Subprocess launched with variable", issue.Medium, issue.High), nil
}
}
case *ast.ValueSpec:
_, valueSpec := ident.Obj.Decl.(*ast.ValueSpec)
if variable && valueSpec {
if !gosec.TryResolve(ident, c) {
return c.NewIssue(n, r.ID(), "Subprocess launched with variable", issue.Medium, issue.High), nil
}
}
// For all variables: flag if not resolvable to a constant
if !gosec.TryResolve(ident, c) {
return c.NewIssue(n, r.ID(), "Subprocess launched with variable", issue.Medium, issue.High), nil
}
}
} else if !gosec.TryResolve(arg, c) {
// the arg is not a constant or a variable but instead a function call or os.Args[i]
// Non-identifier arguments that cannot be resolved
return c.NewIssue(n, r.ID(), "Subprocess launched with a potential tainted input or cmd arguments", issue.Medium, issue.High), nil
}
}

View File

@@ -20,8 +20,9 @@ import (
"crypto/tls"
"fmt"
"go/ast"
"go/token"
"go/types"
"strconv"
"slices"
"github.com/securego/gosec/v2"
"github.com/securego/gosec/v2/issue"
@@ -35,28 +36,33 @@ type insecureConfigTLS struct {
goodCiphers []string
actualMinVersion int64
actualMaxVersion int64
minVersionSet bool
maxVersionSet bool
}
func (t *insecureConfigTLS) ID() string {
return t.MetaData.ID
}
func stringInSlice(a string, list []string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
var tlsVersionMap = map[string]int64{
"VersionTLS10": tls.VersionTLS10,
"VersionTLS11": tls.VersionTLS11,
"VersionTLS12": tls.VersionTLS12,
"VersionTLS13": tls.VersionTLS13,
}
func (t *insecureConfigTLS) mapVersion(version string) int64 {
return tlsVersionMap[version]
}
func (t *insecureConfigTLS) processTLSCipherSuites(n ast.Node, c *gosec.Context) *issue.Issue {
if ciphers, ok := n.(*ast.CompositeLit); ok {
for _, cipher := range ciphers.Elts {
if ident, ok := cipher.(*ast.SelectorExpr); ok {
if !stringInSlice(ident.Sel.Name, t.goodCiphers) {
err := fmt.Sprintf("TLS Bad Cipher Suite: %s", ident.Sel.Name)
return c.NewIssue(ident, t.ID(), err, issue.High, issue.High)
for _, elt := range ciphers.Elts {
if ident, ok := elt.(*ast.SelectorExpr); ok {
cipherName := ident.Sel.Name
if !slices.Contains(t.goodCiphers, cipherName) {
msg := fmt.Sprintf("TLS Bad Cipher Suite: %s", cipherName)
return c.NewIssue(ident, t.ID(), msg, issue.High, issue.High)
}
}
}
@@ -64,148 +70,205 @@ func (t *insecureConfigTLS) processTLSCipherSuites(n ast.Node, c *gosec.Context)
return nil
}
func (t *insecureConfigTLS) resolveTLSVersion(expr ast.Expr, c *gosec.Context) int64 {
if val, err := gosec.GetInt(expr); err == nil {
return val
}
if se, ok := expr.(*ast.SelectorExpr); ok {
if x, ok := se.X.(*ast.Ident); ok {
if ip, ok := gosec.GetImportPath(x.Name, c); ok && ip == "crypto/tls" {
return t.mapVersion(se.Sel.Name)
}
}
}
if id, ok := expr.(*ast.Ident); ok {
obj := c.Info.ObjectOf(id)
if obj != nil {
init := t.findDefinition(obj, c)
if init != nil {
if val, err := gosec.GetInt(init); err == nil {
return val
}
if se, ok := init.(*ast.SelectorExpr); ok {
if x, ok := se.X.(*ast.Ident); ok {
if ip, ok := gosec.GetImportPath(x.Name, c); ok && ip == "crypto/tls" {
return t.mapVersion(se.Sel.Name)
}
}
}
}
}
}
return 0 // unknown / unresolved
}
func (t *insecureConfigTLS) resolveBoolConst(expr ast.Expr, c *gosec.Context) (bool, bool) {
if id, ok := expr.(*ast.Ident); ok {
if id.Name == "true" {
return true, true
}
if id.Name == "false" {
return false, true
}
}
if u, ok := expr.(*ast.UnaryExpr); ok && u.Op == token.NOT {
if op, ok := u.X.(*ast.Ident); ok {
if op.Name == "true" {
return false, true
}
if op.Name == "false" {
return true, true
}
}
}
if id, ok := expr.(*ast.Ident); ok {
obj := c.Info.ObjectOf(id)
if obj != nil {
init := t.findDefinition(obj, c)
if init != nil {
if iid, ok := init.(*ast.Ident); ok {
if iid.Name == "true" {
return true, true
}
if iid.Name == "false" {
return false, true
}
}
if uu, ok := init.(*ast.UnaryExpr); ok && uu.Op == token.NOT {
if op, ok := uu.X.(*ast.Ident); ok {
if op.Name == "true" {
return false, true
}
if op.Name == "false" {
return true, true
}
}
}
}
}
}
return false, false // unknown
}
func (t *insecureConfigTLS) processTLSConfVal(key ast.Expr, value ast.Expr, c *gosec.Context) *issue.Issue {
if ident, ok := key.(*ast.Ident); ok {
switch ident.Name {
case "InsecureSkipVerify":
val, known := t.resolveBoolConst(value, c)
if known && val {
return c.NewIssue(value, t.ID(), "TLS InsecureSkipVerify set to true.", issue.High, issue.High)
}
if !known {
return c.NewIssue(value, t.ID(), "TLS InsecureSkipVerify may be set to true.", issue.High, issue.Low)
}
case "PreferServerCipherSuites":
val, known := t.resolveBoolConst(value, c)
if known && !val {
return c.NewIssue(value, t.ID(), "TLS PreferServerCipherSuites set to false.", issue.Medium, issue.High)
}
if !known {
return c.NewIssue(value, t.ID(), "TLS PreferServerCipherSuites may be set to false.", issue.Medium, issue.Low)
}
case "MinVersion":
t.minVersionSet = true
t.actualMinVersion = t.resolveTLSVersion(value, c)
case "MaxVersion":
t.maxVersionSet = true
t.actualMaxVersion = t.resolveTLSVersion(value, c)
case "CipherSuites":
return t.processTLSCipherSuites(value, c)
}
}
return nil
}
func (t *insecureConfigTLS) processTLSConf(n ast.Node, c *gosec.Context) *issue.Issue {
if kve, ok := n.(*ast.KeyValueExpr); ok {
issue := t.processTLSConfVal(kve.Key, kve.Value, c)
if issue != nil {
return issue
}
} else if assign, ok := n.(*ast.AssignStmt); ok {
return t.processTLSConfVal(kve.Key, kve.Value, c)
}
if assign, ok := n.(*ast.AssignStmt); ok {
if len(assign.Lhs) < 1 || len(assign.Rhs) < 1 {
return nil
}
if selector, ok := assign.Lhs[0].(*ast.SelectorExpr); ok {
issue := t.processTLSConfVal(selector.Sel, assign.Rhs[0], c)
if issue != nil {
return issue
}
return t.processTLSConfVal(selector.Sel, assign.Rhs[0], c)
}
}
return nil
}
func (t *insecureConfigTLS) processTLSConfVal(key ast.Expr, value ast.Expr, c *gosec.Context) *issue.Issue {
if ident, ok := key.(*ast.Ident); ok {
switch ident.Name {
case "InsecureSkipVerify":
if node, ok := value.(*ast.Ident); ok {
if node.Name != "false" {
return c.NewIssue(value, t.ID(), "TLS InsecureSkipVerify set true.", issue.High, issue.High)
}
} else {
// TODO(tk): symbol tab look up to get the actual value
return c.NewIssue(value, t.ID(), "TLS InsecureSkipVerify may be true.", issue.High, issue.Low)
}
case "PreferServerCipherSuites":
if node, ok := value.(*ast.Ident); ok {
if node.Name == "false" {
return c.NewIssue(value, t.ID(), "TLS PreferServerCipherSuites set false.", issue.Medium, issue.High)
}
} else {
// TODO(tk): symbol tab look up to get the actual value
return c.NewIssue(value, t.ID(), "TLS PreferServerCipherSuites may be false.", issue.Medium, issue.Low)
}
case "MinVersion":
if d, ok := value.(*ast.Ident); ok {
obj := d.Obj
if obj == nil {
for _, f := range c.PkgFiles {
obj = f.Scope.Lookup(d.Name)
if obj != nil {
break
}
}
}
if vs, ok := obj.Decl.(*ast.ValueSpec); ok && len(vs.Values) > 0 {
if s, ok := vs.Values[0].(*ast.SelectorExpr); ok {
x := s.X.(*ast.Ident).Name
sel := s.Sel.Name
for _, imp := range c.Pkg.Imports() {
if imp.Name() == x {
tObj := imp.Scope().Lookup(sel)
if cst, ok := tObj.(*types.Const); ok {
// ..got the value check if this can be translated
if minVersion, err := strconv.ParseInt(cst.Val().String(), 0, 64); err == nil {
t.actualMinVersion = minVersion
}
}
}
}
}
if ival, ierr := gosec.GetInt(vs.Values[0]); ierr == nil {
t.actualMinVersion = ival
}
}
} else if ival, ierr := gosec.GetInt(value); ierr == nil {
t.actualMinVersion = ival
} else {
if se, ok := value.(*ast.SelectorExpr); ok {
if pkg, ok := se.X.(*ast.Ident); ok {
if ip, ok := gosec.GetImportPath(pkg.Name, c); ok && ip == "crypto/tls" {
t.actualMinVersion = t.mapVersion(se.Sel.Name)
}
}
}
}
case "MaxVersion":
if ival, ierr := gosec.GetInt(value); ierr == nil {
t.actualMaxVersion = ival
} else {
if se, ok := value.(*ast.SelectorExpr); ok {
if pkg, ok := se.X.(*ast.Ident); ok {
if ip, ok := gosec.GetImportPath(pkg.Name, c); ok && ip == "crypto/tls" {
t.actualMaxVersion = t.mapVersion(se.Sel.Name)
}
}
}
}
case "CipherSuites":
if ret := t.processTLSCipherSuites(value, c); ret != nil {
return ret
}
func (t *insecureConfigTLS) findDefinition(obj types.Object, c *gosec.Context) ast.Expr {
file := gosec.ContainingFile(obj, c)
if file == nil {
return nil
}
var initializer ast.Expr
ast.Inspect(file, func(n ast.Node) bool {
if initializer != nil {
return false
}
}
return nil
}
func (t *insecureConfigTLS) mapVersion(version string) int64 {
var v int64
switch version {
case "VersionTLS13":
v = tls.VersionTLS13
case "VersionTLS12":
v = tls.VersionTLS12
case "VersionTLS11":
v = tls.VersionTLS11
case "VersionTLS10":
v = tls.VersionTLS10
}
return v
switch n := n.(type) {
case *ast.ValueSpec:
for i, name := range n.Names {
if name.Pos() == obj.Pos() && i < len(n.Values) {
initializer = n.Values[i]
return false
}
}
case *ast.AssignStmt:
for i, lhs := range n.Lhs {
if id, ok := lhs.(*ast.Ident); ok && id.Pos() == obj.Pos() && i < len(n.Rhs) {
initializer = n.Rhs[i]
return false
}
}
}
return true
})
return initializer
}
func (t *insecureConfigTLS) checkVersion(n ast.Node, c *gosec.Context) *issue.Issue {
if t.actualMaxVersion == 0 && t.actualMinVersion >= t.MinVersion {
// no warning is generated since the min version is greater than the secure min version
return nil
}
if t.actualMinVersion < t.MinVersion {
// Flag explicitly low MinVersion (including explicit 0)
if t.minVersionSet && t.actualMinVersion < t.MinVersion {
return c.NewIssue(n, t.ID(), "TLS MinVersion too low.", issue.High, issue.High)
}
if t.actualMaxVersion < t.MaxVersion {
return c.NewIssue(n, t.ID(), "TLS MaxVersion too low.", issue.High, issue.High)
// Handle MaxVersion
if t.maxVersionSet {
// Special case for explicit MaxVersion: 0 (default latest) - suppress warning if MinVersion is securely set
if t.actualMaxVersion == 0 {
if t.minVersionSet && t.actualMinVersion >= t.MinVersion {
return nil
}
// Otherwise treat explicit 0 as potentially insecure (fall through to flag)
}
// Flag if explicitly capped too low (non-zero low values always flagged)
if t.actualMaxVersion < t.MaxVersion {
return c.NewIssue(n, t.ID(), "TLS MaxVersion too low.", issue.High, issue.High)
}
}
return nil
}
func (t *insecureConfigTLS) resetVersion() {
t.actualMaxVersion = 0
t.actualMinVersion = 0
t.actualMaxVersion = 0
t.minVersionSet = false
t.maxVersionSet = false
}
func (t *insecureConfigTLS) Match(n ast.Node, c *gosec.Context) (*issue.Issue, error) {
@@ -213,27 +276,26 @@ func (t *insecureConfigTLS) Match(n ast.Node, c *gosec.Context) (*issue.Issue, e
actualType := c.Info.TypeOf(complit.Type)
if actualType != nil && actualType.String() == t.requiredType {
for _, elt := range complit.Elts {
issue := t.processTLSConf(elt, c)
if issue != nil {
if issue := t.processTLSConf(elt, c); issue != nil {
return issue, nil
}
}
issue := t.checkVersion(complit, c)
if issue := t.checkVersion(complit, c); issue != nil {
return issue, nil
}
t.resetVersion()
return issue, nil
return nil, nil
}
} else {
if assign, ok := n.(*ast.AssignStmt); ok && len(assign.Lhs) > 0 {
if selector, ok := assign.Lhs[0].(*ast.SelectorExpr); ok {
actualType := c.Info.TypeOf(selector.X)
if actualType != nil && actualType.String() == t.requiredType {
issue := t.processTLSConf(assign, c)
if issue != nil {
return issue, nil
}
}
}
if assign, ok := n.(*ast.AssignStmt); ok && len(assign.Lhs) > 0 {
if selector, ok := assign.Lhs[0].(*ast.SelectorExpr); ok {
actualType := c.Info.TypeOf(selector.X)
if actualType != nil && actualType.String() == t.requiredType {
return t.processTLSConf(assign, c), nil
}
}
}
return nil, nil
}

View File

@@ -427,5 +427,147 @@ func main() {
}
defer stmt.Close()
}
`}, 0, gosec.NewConfig()},
{[]string{`
// Safe verb (%d) with tainted input - no string injection risk
package main
import (
"database/sql"
"fmt"
"os"
"strconv"
)
func main() {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
panic(err)
}
id, _ := strconv.Atoi(os.Args[1]) // tainted but used with %d
q := fmt.Sprintf("SELECT * FROM foo WHERE id = %d", id)
rows, err := db.Query(q)
if err != nil {
panic(err)
}
defer rows.Close()
}
`}, 0, gosec.NewConfig()},
{[]string{`
// Mixed args: unsafe %s (tainted) + safe %d (constant)
package main
import (
"database/sql"
"fmt"
"os"
)
func main() {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
panic(err)
}
q := fmt.Sprintf("SELECT * FROM %s WHERE id = %d", os.Args[1], 42) // tainted table + safe int
rows, err := db.Query(q)
if err != nil {
panic(err)
}
defer rows.Close()
}
`}, 1, gosec.NewConfig()},
{[]string{`
// All args constant but unsafe verb present - safe
package main
import (
"database/sql"
"fmt"
)
const name = "admin"
func main() {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
panic(err)
}
q := fmt.Sprintf("SELECT * FROM users WHERE name = '%s'", name)
rows, err := db.Query(q)
if err != nil {
panic(err)
}
defer rows.Close()
}
`}, 0, gosec.NewConfig()},
{[]string{`
// Formatter from concatenation - risky
package main
import (
"database/sql"
"fmt"
"os"
)
func main() {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
panic(err)
}
base := "SELECT * FROM foo WHERE"
q := fmt.Sprintf(base + " name = '%s'", os.Args[1])
rows, err := db.Query(q)
if err != nil {
panic(err)
}
defer rows.Close()
}
`}, 1, gosec.NewConfig()},
{[]string{`
// No unsafe % verb but SQL pattern + tainted concat - G202, not G201
package main
import (
"database/sql"
"os"
)
func main() {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
panic(err)
}
q := "SELECT * FROM foo WHERE name = " + os.Args[1] // concat, no %
rows, err := db.Query(q)
if err != nil {
panic(err)
}
defer rows.Close()
}
`}, 0, gosec.NewConfig()}, // G201 should NOT flag (G202 does)
{[]string{`
// Fprintf to os.Stderr - no issue
package main
import (
"database/sql"
"fmt"
"os"
)
func main() {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
panic(err)
}
q := fmt.Sprintf("SELECT * FROM foo WHERE name = '%s'", os.Args[1])
fmt.Fprintf(os.Stderr, "Debug query: %s\n", q) // log, not exec
rows, err := db.Query("SELECT * FROM foo")
if err != nil {
panic(err)
}
defer rows.Close()
}
`}, 0, gosec.NewConfig()},
}

View File

@@ -335,5 +335,163 @@ func main() {
}
defer rows.Close()
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main
import (
"database/sql"
"os"
)
func main() {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
panic(err)
}
query := "SELECT * FROM album WHERE id = "
query += os.Args[0]
rows, err := db.Query(query)
if err != nil {
panic(err)
}
defer rows.Close()
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main
import (
"fmt"
"os"
)
func main() {
query := "SELECT * FROM album WHERE id = "
query += os.Args[0]
fmt.Println(query)
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
import (
"database/sql"
"os"
)
func main() {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
panic(err)
}
query := "SELECT * FROM album WHERE id = "
query = query + os.Args[0] // risky reassignment concatenation
rows, err := db.Query(query)
if err != nil {
panic(err)
}
defer rows.Close()
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main
import (
"database/sql"
)
func main() {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
panic(err)
}
query := "SELECT * FROM album WHERE id = "
query = query + "42" // safe literal reassignment concatenation
rows, err := db.Query(query)
if err != nil {
panic(err)
}
defer rows.Close()
}
`}, 0, gosec.NewConfig()},
{[]string{`
// Shadowing edge case: tainted mutation on shadowed variable - should NOT flag
// The outer 'query' is safe and passed to db.Query.
// The inner shadowed 'query' is mutated with tainted input (irrelevant).
package main
import (
"database/sql"
"os"
)
func main() {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
panic(err)
}
query := "SELECT * FROM foo WHERE id = 42" // safe outer query
{
query := "base" // shadows outer query
query += os.Args[1] // tainted mutation on shadow - should be ignored
_ = query // prevent unused warning
}
rows, err := db.Query(query) // uses safe outer query
if err != nil {
panic(err)
}
defer rows.Close()
}
`}, 0, gosec.NewConfig()},
{[]string{`
// Shadowing edge case: no mutation on shadow, safe outer - regression guard
package main
import (
"database/sql"
)
func main() {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
panic(err)
}
query := "SELECT * FROM foo WHERE id = 42"
{
query := "shadowed but unused"
_ = query
}
rows, err := db.Query(query)
if err != nil {
panic(err)
}
defer rows.Close()
}
`}, 0, gosec.NewConfig()},
{[]string{`
// package-level SQL string with tainted concatenation in init()
package main
import (
"os"
)
var query string = "SELECT * FROM foo WHERE name = "
func init() {
query += os.Args[1]
}
`, `
package main
import (
"database/sql"
)
func main() {
db, _ := sql.Open("sqlite3", ":memory:")
_, _ = db.Query(query)
}
`}, 1, gosec.NewConfig()},
}

View File

@@ -278,4 +278,41 @@ func main() {
_ = exec.Command(exe, args...)
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
import (
"os/exec"
)
// Direct use of a function parameter in exec.Command.
// This is clearly tainted input (parameter from caller, potentially user-controlled).
func vulnerable(command string) {
// Dangerous pattern: passing unsanitized input to a shell
_ = exec.Command("bash", "-c", command)
}
func main() {
// In real scenarios, this could be user input (e.g., via flag, HTTP param, etc.)
vulnerable("echo safe")
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main
import (
"os/exec"
)
// Indirect use: assign parameter to local variable before use.
// Included for comparison/regression testing.
func vulnerable(command string) {
cmdStr := command // local assignment
_ = exec.Command("bash", "-c", cmdStr)
}
func main() {
vulnerable("echo safe")
}
`}, 1, gosec.NewConfig()},
}

View File

@@ -293,4 +293,90 @@ func main() {
_ = cryptotls.Config{MinVersion: cryptotls.VersionTLS12}
}
`}, 0, gosec.NewConfig()},
{[]string{`
// InsecureSkipVerify with unary NOT (direct !false → true, high confidence)
package main
import "crypto/tls"
func main() {
_ = &tls.Config{InsecureSkipVerify: !false}
}
`}, 1, gosec.NewConfig()},
{[]string{`
// InsecureSkipVerify with unary NOT (direct !true → false, no issue)
package main
import "crypto/tls"
func main() {
_ = &tls.Config{InsecureSkipVerify: !true}
}
`}, 0, gosec.NewConfig()},
{[]string{`
// InsecureSkipVerify via const with NOT (resolves to true, high confidence)
package main
import "crypto/tls"
const skipVerify = !false
func main() {
_ = &tls.Config{InsecureSkipVerify: skipVerify}
}
`}, 1, gosec.NewConfig()},
{[]string{`
// PreferServerCipherSuites false (direct, medium severity)
package main
import "crypto/tls"
func main() {
_ = &tls.Config{PreferServerCipherSuites: false}
}
`}, 1, gosec.NewConfig()},
{[]string{`
// PreferServerCipherSuites with !true (resolves to false)
package main
import "crypto/tls"
func main() {
_ = &tls.Config{PreferServerCipherSuites: !true}
}
`}, 1, gosec.NewConfig()},
{[]string{`
// PreferServerCipherSuites true (no issue)
package main
import "crypto/tls"
func main() {
_ = &tls.Config{PreferServerCipherSuites: true}
}
`}, 0, gosec.NewConfig()},
{[]string{`
// MaxVersion explicitly low via variable
package main
import "crypto/tls"
func main() {
var lowMax uint16 = tls.VersionTLS10
_ = &tls.Config{MaxVersion: lowMax}
}
`}, 1, gosec.NewConfig()},
{[]string{`
// PreferServerCipherSuites unknown → low-confidence
package main
import "crypto/tls"
var prefer bool // unresolved
func main() {
_ = &tls.Config{PreferServerCipherSuites: prefer}
}
`}, 1, gosec.NewConfig()},
}