mirror of
https://github.com/securego/gosec.git
synced 2026-01-15 01:33:41 +08:00
Compare commits
3 Commits
8a5404eabf
...
649e2c8da4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
649e2c8da4 | ||
|
|
35a92b49d5 | ||
|
|
bc9d2bc879 |
72
helpers.go
72
helpers.go
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)}
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
376
rules/sql.go
376
rules/sql.go
@@ -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)}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
352
rules/tls.go
352
rules/tls.go
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()},
|
||||
}
|
||||
|
||||
@@ -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()},
|
||||
}
|
||||
|
||||
@@ -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()},
|
||||
}
|
||||
|
||||
@@ -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()},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user