Compare commits

...

3 Commits

Author SHA1 Message Date
oittaa
833d7919e0 refactor(g115): improve coverage (#1462) 2026-01-12 11:37:18 +01:00
oittaa
0cc9e01a9d Refine G407 to improve detection and coverage of hardcoded nonces (#1460)
* Refine G407 to improve detection and coverage of hardcoded nonces

* chore: consolidate common analyzer patterns into util.go and improve G602 coverage

* Optimize G602 and G115 with state caching and regex pre-compilation

* Improve G115 overflow detection and fix false positives and false negatives

* golangci-lint workaround
2026-01-12 09:56:55 +01:00
renovate[bot]
303f84d111 chore(deps): update all dependencies (#1461)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-12 09:51:22 +01:00
12 changed files with 3293 additions and 695 deletions

187
analyzers/bench_test.go Normal file
View File

@@ -0,0 +1,187 @@
package analyzers_test
import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/buildssa"
"golang.org/x/tools/go/analysis/passes/ctrlflow"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/packages"
"github.com/securego/gosec/v2"
"github.com/securego/gosec/v2/analyzers"
"github.com/securego/gosec/v2/testutils"
)
func benchmarkAnalyzerStress(b *testing.B, analyzerID string, generator func() string) {
logger, _ := testutils.NewLogger()
code := generator()
// SETUP: Create temp dir and main.go
tmpDir, err := os.MkdirTemp("", "gosec_bench")
if err != nil {
b.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
mainGo := filepath.Join(tmpDir, "main.go")
if err := os.WriteFile(mainGo, []byte(code), 0o600); err != nil {
b.Fatalf("failed to write main.go: %v", err)
}
// Create a dummy go.mod to ensure we are in a module
goMod := filepath.Join(tmpDir, "go.mod")
if err := os.WriteFile(goMod, []byte("module bench\n\ngo 1.24\n"), 0o600); err != nil {
b.Fatalf("failed to write go.mod: %v", err)
}
conf := &packages.Config{
Mode: gosec.LoadMode,
Dir: tmpDir,
}
pkgs, err := packages.Load(conf, ".")
if err != nil {
b.Fatalf("failed to load package: %v", err)
}
if len(pkgs) == 0 {
b.Fatalf("no packages loaded")
}
if len(pkgs[0].Errors) > 0 {
b.Fatalf("errors loading package: %v", pkgs[0].Errors)
}
// Prepare analysis context
pass := &analysis.Pass{
Fset: pkgs[0].Fset,
Files: pkgs[0].Syntax,
Pkg: pkgs[0].Types,
TypesInfo: pkgs[0].TypesInfo,
TypesSizes: pkgs[0].TypesSizes,
ResultOf: make(map[*analysis.Analyzer]any),
Report: func(d analysis.Diagnostic) {},
}
pass.Analyzer = inspect.Analyzer
i, _ := inspect.Analyzer.Run(pass)
pass.ResultOf[inspect.Analyzer] = i
pass.Analyzer = ctrlflow.Analyzer
cf, _ := ctrlflow.Analyzer.Run(pass)
pass.ResultOf[ctrlflow.Analyzer] = cf
pass.Analyzer = buildssa.Analyzer
ssaRes, err := buildssa.Analyzer.Run(pass)
if err != nil {
b.Fatalf("failed to build SSA: %v", err)
}
ssaResult := ssaRes.(*buildssa.SSA)
if len(ssaResult.SrcFuncs) == 0 {
b.Fatalf("SSA has 0 source functions.")
}
// Find targeted analyzer
var target *analysis.Analyzer
analyzerList := analyzers.Generate(false)
if def, ok := analyzerList.Analyzers[analyzerID]; ok {
target = def.Create(def.ID, def.Description)
} else {
b.Fatalf("analyzer %s not found", analyzerID)
}
resultMap := map[*analysis.Analyzer]any{
buildssa.Analyzer: &analyzers.SSAAnalyzerResult{
Config: gosec.NewConfig(),
Logger: logger,
SSA: ssaResult,
},
}
runPass := &analysis.Pass{
Analyzer: target,
Fset: pkgs[0].Fset,
Files: pkgs[0].Syntax,
Pkg: pkgs[0].Types,
TypesInfo: pkgs[0].TypesInfo,
TypesSizes: pkgs[0].TypesSizes,
ResultOf: resultMap,
Report: func(d analysis.Diagnostic) {},
}
b.ResetTimer()
for range b.N {
_, err := target.Run(runPass)
if err != nil {
b.Fatalf("failed to run analyzer: %v", err)
}
}
}
// Generators
func generateG115Deep(nesting, conversions int) string {
var sb strings.Builder
sb.WriteString("package main\nimport \"math\"\nfunc run_stress(x int64) {\n")
for i := range nesting {
fmt.Fprintf(&sb, "if x > %d && x < math.MaxInt64 {\n", i)
}
for range conversions {
fmt.Fprintf(&sb, "_ = int8(x)\n")
}
for range nesting {
sb.WriteString("}\n")
}
sb.WriteString("}\n")
return sb.String()
}
func generateG602Wide(levels, accesses int) string {
var sb strings.Builder
sb.WriteString("package main\nfunc run_stress() {\n")
sb.WriteString("s := make([]byte, 100000)\n")
for i := range levels {
fmt.Fprintf(&sb, "s%d := s[%d:]\n", i, i)
for j := range accesses {
fmt.Fprintf(&sb, "_ = s%d[%d]\n", i, j)
fmt.Fprintf(&sb, "_ = s%d[%d]\n", i, j+1)
}
}
sb.WriteString("}\n")
return sb.String()
}
func generateG407Stress(depth int) string {
var sb strings.Builder
sb.WriteString("package main\nimport \"crypto/cipher\"\nfunc run_stress(gcm cipher.AEAD, data []byte) {\n")
sb.WriteString("nonce := []byte(\"hardcoded_nonce_value\")\n")
// Chain of assignments
for i := range depth {
fmt.Fprintf(&sb, "n%d := nonce\n", i)
if i > 0 {
fmt.Fprintf(&sb, "n%d = n%d\n", i, i-1)
}
}
// Use the last nonce in the chain
fmt.Fprintf(&sb, "gcm.Seal(nil, n%d, data, nil)\n", depth-1)
fmt.Fprintf(&sb, "}\n")
return sb.String()
}
// Benchmarks (Logic Only)
func BenchmarkAnalysisG115_Deep(b *testing.B) {
benchmarkAnalyzerStress(b, "G115", func() string { return generateG115Deep(300, 1000) })
}
func BenchmarkAnalysisG602_Wide(b *testing.B) {
benchmarkAnalyzerStress(b, "G602", func() string { return generateG602Wide(500, 200) })
}
func BenchmarkAnalysisG407_Deep(b *testing.B) {
benchmarkAnalyzerStress(b, "G407", func() string { return generateG407Stress(1000) })
}

File diff suppressed because it is too large Load Diff

View File

@@ -12,12 +12,12 @@ var _ = Describe("ParseIntType", func() {
Context("with valid input", func() {
DescribeTable("should correctly parse and calculate bounds for",
func(intType string, expectedSigned bool, expectedSize int, expectedMin int, expectedMax uint) {
result, err := parseIntType(intType)
result, err := ParseIntType(intType)
Expect(err).NotTo(HaveOccurred())
Expect(result.signed).To(Equal(expectedSigned))
Expect(result.size).To(Equal(expectedSize))
Expect(result.min).To(Equal(expectedMin))
Expect(result.max).To(Equal(expectedMax))
Expect(result.Signed).To(Equal(expectedSigned))
Expect(result.Size).To(Equal(expectedSize))
Expect(result.Min).To(Equal(expectedMin))
Expect(result.Max).To(Equal(expectedMax))
},
Entry("uint8", "uint8", false, 8, 0, uint(math.MaxUint8)),
Entry("int8", "int8", true, 8, math.MinInt8, uint(math.MaxInt8)),
@@ -30,20 +30,20 @@ var _ = Describe("ParseIntType", func() {
)
It("should use system's int size for 'int' and 'uint'", func() {
intResult, err := parseIntType("int")
intResult, err := ParseIntType("int")
Expect(err).NotTo(HaveOccurred())
Expect(intResult.size).To(Equal(strconv.IntSize))
Expect(intResult.Size).To(Equal(strconv.IntSize))
uintResult, err := parseIntType("uint")
uintResult, err := ParseIntType("uint")
Expect(err).NotTo(HaveOccurred())
Expect(uintResult.size).To(Equal(strconv.IntSize))
Expect(uintResult.Size).To(Equal(strconv.IntSize))
})
})
Context("with invalid input", func() {
DescribeTable("should return an error for",
func(intType string, expectedErrorString string) {
_, err := parseIntType(intType)
_, err := ParseIntType(intType)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring(expectedErrorString))
},

View File

@@ -15,7 +15,6 @@
package analyzers
import (
"errors"
"fmt"
"go/token"
"strings"
@@ -29,6 +28,18 @@ import (
const defaultIssueDescription = "Use of hardcoded IV/nonce for encryption"
// tracked holds the function name as key, the number of arguments that the function accepts,
// and the index of the argument that is the nonce/IV.
// Example: "crypto/cipher.NewCBCEncrypter": {2, 1} means the function accepts 2 arguments,
// and the nonce arg is at index 1 (the second argument).
var tracked = map[string][]int{
"(crypto/cipher.AEAD).Seal": {4, 1},
"crypto/cipher.NewCBCEncrypter": {2, 1},
"crypto/cipher.NewCFBEncrypter": {2, 1},
"crypto/cipher.NewCTR": {2, 1},
"crypto/cipher.NewOFB": {2, 1},
}
func newHardCodedNonce(id string, description string) *analysis.Analyzer {
return &analysis.Analyzer{
Name: id,
@@ -38,32 +49,18 @@ func newHardCodedNonce(id string, description string) *analysis.Analyzer {
}
}
func runHardCodedNonce(pass *analysis.Pass) (interface{}, error) {
func runHardCodedNonce(pass *analysis.Pass) (any, error) {
ssaResult, err := getSSAResult(pass)
if err != nil {
return nil, fmt.Errorf("building ssa representation: %w", err)
return nil, err
}
// Holds the function name as key, the number of arguments that the function accepts, and at which index of those accepted arguments is the nonce/IV
// Example "Test" 3, 1 -- means the function "Test" which accepts 3 arguments, and has the nonce arg as second argument
calls := map[string][]int{
"(crypto/cipher.AEAD).Seal": {4, 1},
"crypto/cipher.NewCBCEncrypter": {2, 1},
"crypto/cipher.NewCFBEncrypter": {2, 1},
"crypto/cipher.NewCTR": {2, 1},
"crypto/cipher.NewOFB": {2, 1},
}
ssaPkgFunctions := ssaResult.SSA.SrcFuncs
args := getArgsFromTrackedFunctions(ssaPkgFunctions, calls)
if args == nil {
return nil, errors.New("no tracked functions found, resulting in no variables to track")
}
state := newAnalysisState(pass, ssaResult.SSA.SrcFuncs)
args := state.getInitialArgs(tracked)
var issues []*issue.Issue
for _, arg := range args {
if arg == nil {
continue
}
i, err := raiseIssue(*arg, calls, ssaPkgFunctions, pass, "")
for _, argInfo := range args {
i, err := state.raiseIssue(argInfo.val, "", make(map[ssa.Value]bool), argInfo.instr)
if err != nil {
return issues, fmt.Errorf("raising issue error: %w", err)
}
@@ -72,180 +69,374 @@ func runHardCodedNonce(pass *analysis.Pass) (interface{}, error) {
return issues, nil
}
func raiseIssue(val ssa.Value, funcsToTrack map[string][]int, ssaFuncs []*ssa.Function,
pass *analysis.Pass, issueDescription string,
type usageResult struct {
dyn bool
hard bool
}
type analysisState struct {
pass *analysis.Pass
ssaFuncs []*ssa.Function
usageCache map[ssa.Value]*usageResult
funcCache map[*ssa.Function]bool
visitedFuncs map[*ssa.Function]bool
callerMap map[string][]*ssa.Call
bufferLenCache map[ssa.Value]int64
depth int
}
type ssaValueAndInstr struct {
val ssa.Value
instr ssa.Instruction
}
func newAnalysisState(pass *analysis.Pass, funcs []*ssa.Function) *analysisState {
s := &analysisState{
pass: pass,
ssaFuncs: funcs,
usageCache: make(map[ssa.Value]*usageResult),
funcCache: make(map[*ssa.Function]bool),
visitedFuncs: make(map[*ssa.Function]bool),
callerMap: BuildCallerMap(funcs),
bufferLenCache: make(map[ssa.Value]int64),
}
return s
}
// getInitialArgs returns a list of arguments and their corresponding instructions
// for all call sites identified in the tracked map.
func (s *analysisState) getInitialArgs(tracked map[string][]int) []ssaValueAndInstr {
var result []ssaValueAndInstr
for name, info := range tracked {
if calls, ok := s.callerMap[name]; ok {
for _, c := range calls {
if len(c.Call.Args) == info[0] {
result = append(result, ssaValueAndInstr{
val: c.Call.Args[info[1]],
instr: c,
})
}
}
}
}
return result
}
// raiseIssue recursively analyzes the usage of a value and returns a list of issues
// if it's found to be hardcoded or otherwise insecure.
func (s *analysisState) raiseIssue(val ssa.Value, issueDescription string,
visitedParams map[ssa.Value]bool, fromInstr ssa.Instruction,
) ([]*issue.Issue, error) {
if visitedParams[val] {
return nil, nil
}
visitedParams[val] = true
foundDyn, foundHard := s.analyzeUsage(val)
if foundDyn && !foundHard {
return nil, nil
}
if issueDescription == "" {
issueDescription = defaultIssueDescription
}
var err error
var allIssues []*issue.Issue
var issues []*issue.Issue
switch valType := (val).(type) {
switch v := val.(type) {
case *ssa.Slice:
issueDescription += " by passing hardcoded slice/array"
issues, err = iterateThroughReferrers(val, funcsToTrack, pass.Analyzer.Name, issueDescription, pass.Fset, issue.High)
allIssues = append(allIssues, issues...)
case *ssa.UnOp:
// Check if it's a dereference operation (a.k.a pointer)
if valType.Op == token.MUL {
issueDescription += " by passing pointer which points to hardcoded variable"
issues, err = iterateThroughReferrers(val, funcsToTrack, pass.Analyzer.Name, issueDescription, pass.Fset, issue.Low)
allIssues = append(allIssues, issues...)
if s.isHardcoded(v.X) {
issueDescription += " by passing hardcoded slice/array"
}
// When the value assigned to a variable is a function call.
// It goes and check if this function contains call to crypto/rand.Read
// in it's body(Assuming that calling crypto/rand.Read in a function,
// is used for the generation of nonce/iv )
case *ssa.Call:
if callValue := valType.Call.Value; callValue != nil {
if calledFunction, ok := callValue.(*ssa.Function); ok {
if contains, funcErr := isFuncContainsCryptoRand(calledFunction); !contains && funcErr == nil {
issueDescription += " by passing a value from function which doesn't use crypto/rand"
issues, err = iterateThroughReferrers(val, funcsToTrack, pass.Analyzer.Name, issueDescription, pass.Fset, issue.Medium)
allIssues = append(allIssues, issues...)
} else if funcErr != nil {
err = funcErr
}
return s.raiseIssue(v.X, issueDescription, visitedParams, fromInstr)
case *ssa.UnOp:
if v.Op == token.MUL {
if s.isHardcoded(v.X) {
issueDescription += " by passing pointer which points to hardcoded variable"
}
return s.raiseIssue(v.X, issueDescription, visitedParams, fromInstr)
}
case *ssa.Convert:
if v.Type().String() == "[]byte" && v.X.Type().String() == "string" {
if s.isHardcoded(v.X) {
issueDescription += " by passing converted string"
}
}
// only checks from strings->[]byte
// might need to add additional types
case *ssa.Convert:
if valType.Type().String() == "[]byte" && valType.X.Type().String() == "string" {
issueDescription += " by passing converted string"
issues, err = iterateThroughReferrers(val, funcsToTrack, pass.Analyzer.Name, issueDescription, pass.Fset, issue.High)
allIssues = append(allIssues, issues...)
return s.raiseIssue(v.X, issueDescription, visitedParams, fromInstr)
case *ssa.Const:
issueDescription += " by passing hardcoded constant"
allIssues = append(allIssues, newIssue(s.pass.Analyzer.Name, issueDescription, s.pass.Fset, fromInstr.Pos(), issue.High, issue.High))
case *ssa.Global:
issueDescription += " by passing hardcoded global"
allIssues = append(allIssues, newIssue(s.pass.Analyzer.Name, issueDescription, s.pass.Fset, fromInstr.Pos(), issue.High, issue.High))
case *ssa.Alloc:
switch v.Comment {
case "slicelit":
issueDescription += " by passing hardcoded slice literal"
allIssues = append(allIssues, newIssue(s.pass.Analyzer.Name, issueDescription, s.pass.Fset, fromInstr.Pos(), issue.High, issue.High))
case "makeslice":
foundDyn, foundHard := s.analyzeUsage(v)
if foundHard {
issueDescription += " by passing a buffer from make modified with hardcoded values"
allIssues = append(allIssues, newIssue(s.pass.Analyzer.Name, issueDescription, s.pass.Fset, fromInstr.Pos(), issue.High, issue.High))
} else if !foundDyn {
issueDescription += " by passing a zeroed buffer from make"
allIssues = append(allIssues, newIssue(s.pass.Analyzer.Name, issueDescription, s.pass.Fset, fromInstr.Pos(), issue.High, issue.High))
}
}
case *ssa.Call:
if s.isHardcoded(v) {
issueDescription += " by passing a value from function which returns hardcoded value"
allIssues = append(allIssues, newIssue(s.pass.Analyzer.Name, issueDescription, s.pass.Fset, fromInstr.Pos(), issue.High, issue.High))
}
case *ssa.Parameter:
// arg given to tracked function is wrapped in another function, example:
// func encrypt(..,nonce,...){
// aesgcm.Seal(nonce)
// }
// save parameter position, by checking the name of the variable used in
// tracked functions and comparing it with the name of the arg
if valType.Parent() != nil {
trackedFunctions := make(map[string][]int)
for index, funcArgs := range valType.Parent().Params {
if funcArgs.Name() == valType.Name() && funcArgs.Type() == valType.Type() {
trackedFunctions[valType.Parent().String()] = []int{len(valType.Parent().Params), index}
if v.Parent() != nil {
parentName := v.Parent().String()
paramIdx := -1
for i, p := range v.Parent().Params {
if p == v {
paramIdx = i
break
}
}
args := getArgsFromTrackedFunctions(ssaFuncs, trackedFunctions)
issueDescription += " by passing a parameter to a function and"
// recursively backtrack to where the origin of a variable passed to multiple functions is
for _, arg := range args {
if arg == nil {
continue
}
issues, err = raiseIssue(*arg, trackedFunctions, ssaFuncs, pass, issueDescription)
allIssues = append(allIssues, issues...)
}
}
}
return allIssues, err
}
// iterateThroughReferrers iterates through all places that use the `variable` argument and check if it's used in one of the tracked functions.
func iterateThroughReferrers(variable ssa.Value, funcsToTrack map[string][]int,
analyzerID string, issueDescription string,
fileSet *token.FileSet, issueConfidence issue.Score,
) ([]*issue.Issue, error) {
if funcsToTrack == nil || variable == nil || analyzerID == "" || issueDescription == "" || fileSet == nil {
return nil, errors.New("received a nil object")
}
var gosecIssues []*issue.Issue
refs := variable.Referrers()
if refs == nil {
return gosecIssues, nil
}
// Go through all functions that use the given arg variable
for _, ref := range *refs {
// Iterate through the functions we are interested
for trackedFunc := range funcsToTrack {
// Split the functions we are interested in, by the '.' because we will use the function name to do the comparison
// MIGHT GIVE SOME FALSE POSITIVES THIS WAY
trackedFuncParts := strings.Split(trackedFunc, ".")
trackedFuncPartsName := trackedFuncParts[len(trackedFuncParts)-1]
if strings.Contains(ref.String(), trackedFuncPartsName) {
gosecIssues = append(gosecIssues, newIssue(analyzerID, issueDescription, fileSet, ref.Pos(), issue.High, issueConfidence))
}
}
}
return gosecIssues, nil
}
// isFuncContainsCryptoRand checks whether a function contains a call to crypto/rand.Read in it's function body.
func isFuncContainsCryptoRand(funcCall *ssa.Function) (bool, error) {
if funcCall == nil {
return false, errors.New("passed ssa.Function object is nil")
}
for _, block := range funcCall.Blocks {
for _, instr := range block.Instrs {
if call, ok := instr.(*ssa.Call); ok {
if calledFunction, ok := call.Call.Value.(*ssa.Function); ok {
if calledFunction.Pkg != nil && calledFunction.Pkg.Pkg.Path() == "crypto/rand" && calledFunction.Name() == "Read" {
return true, nil
}
}
}
}
}
return false, nil
}
func addToVarsMap(value ssa.Value, mapToAddTo map[string]*ssa.Value) {
var parent string
if value.Parent() != nil {
parent = value.Parent().String()
}
key := value.Name() + value.Type().String() + value.String() + parent
mapToAddTo[key] = &value
}
func isContainedInMap(value ssa.Value, mapToCheck map[string]*ssa.Value) bool {
var parent string
if value.Parent() != nil {
parent = value.Parent().String()
}
key := value.Name() + value.Type().String() + value.String() + parent
_, contained := mapToCheck[key]
return contained
}
func getArgsFromTrackedFunctions(ssaFuncs []*ssa.Function, trackedFunc map[string][]int) map[string]*ssa.Value {
values := make(map[string]*ssa.Value)
for _, pkgFunc := range ssaFuncs {
for _, funcBlock := range pkgFunc.Blocks {
for _, funcBlocInstr := range funcBlock.Instrs {
iterateTrackedFunctionsAndAddArgs(funcBlocInstr, trackedFunc, values)
}
}
}
return values
}
func iterateTrackedFunctionsAndAddArgs(funcBlocInstr ssa.Instruction, trackedFunc map[string][]int, values map[string]*ssa.Value) {
if funcCall, ok := (funcBlocInstr).(*ssa.Call); ok {
for trackedFuncName, trackedFuncArgsInfo := range trackedFunc {
// only process functions that have the same number of arguments as the ones we track
if len(funcCall.Call.Args) == trackedFuncArgsInfo[0] {
tmpArg := funcCall.Call.Args[trackedFuncArgsInfo[1]]
// check if the function is called from an object or directly from the package
if funcCall.Call.Method != nil {
if methodFullname := funcCall.Call.Method.FullName(); methodFullname == trackedFuncName {
if !isContainedInMap(tmpArg, values) {
addToVarsMap(tmpArg, values)
if paramIdx != -1 {
numParams := len(v.Parent().Params)
issueDescription += " by passing a parameter to a function and"
if callers, ok := s.callerMap[parentName]; ok {
for _, c := range callers {
if len(c.Call.Args) == numParams {
issues, _ := s.raiseIssue(c.Call.Args[paramIdx], issueDescription, visitedParams, c)
allIssues = append(allIssues, issues...)
}
}
} else if funcCall.Call.Value.String() == trackedFuncName {
if !isContainedInMap(tmpArg, values) {
addToVarsMap(tmpArg, values)
}
}
}
}
return allIssues, nil
}
func (s *analysisState) isHardcoded(val ssa.Value) bool {
if s.depth > MaxDepth {
return false
}
s.depth++
defer func() { s.depth-- }()
switch v := val.(type) {
case *ssa.Const, *ssa.Global:
return true
case *ssa.Convert:
return s.isHardcoded(v.X)
case *ssa.Slice:
return s.isHardcoded(v.X)
case *ssa.Alloc:
if v.Comment == "slicelit" {
return true
}
if v.Comment == "makeslice" {
dyn, hard := s.analyzeUsage(v)
return hard || !dyn
}
case *ssa.Call:
if fn, ok := v.Call.Value.(*ssa.Function); ok {
if res, ok := s.funcCache[fn]; ok {
return res
}
if s.visitedFuncs[fn] {
return false
}
s.visitedFuncs[fn] = true
res := s.isFuncReturnsHardcoded(fn)
s.funcCache[fn] = res
delete(s.visitedFuncs, fn)
return res
}
case *ssa.Parameter:
return false
}
return false
}
func (s *analysisState) isFuncReturnsHardcoded(fn *ssa.Function) bool {
for _, block := range fn.Blocks {
for _, instr := range block.Instrs {
if ret, ok := instr.(*ssa.Return); ok {
for _, res := range ret.Results {
if s.isHardcoded(res) {
return true
}
}
}
}
}
return false
}
// analyzeUsage performs data-flow analysis to determine if a value is derived from
// a dynamic source (like crypto/rand) or if it's fixed/hardcoded.
func (s *analysisState) analyzeUsage(val ssa.Value) (bool, bool) {
if val == nil || s.depth > MaxDepth {
return false, false
}
if res, ok := s.usageCache[val]; ok {
if res == nil { // currently visiting
return false, false
}
return res.dyn, res.hard
}
s.usageCache[val] = nil // mark as visiting
s.depth++
defer func() { s.depth-- }()
dyn := false
hard := false
switch v := val.(type) {
case *ssa.Const, *ssa.Global:
hard = true
case *ssa.Alloc:
if v.Comment == "slicelit" {
hard = true
}
case *ssa.Convert:
d, h := s.analyzeUsage(v.X)
dyn, hard = dyn || d, hard || h
case *ssa.Slice:
d, h := s.analyzeUsage(v.X)
if s.isFullSlice(v) {
dyn = dyn || d
}
hard = hard || h
case *ssa.UnOp:
if v.Op == token.MUL {
d, h := s.analyzeUsage(v.X)
dyn, hard = dyn || d, hard || h
}
case *ssa.Call:
if s.isHardcoded(v) {
hard = true
}
case *ssa.Parameter:
if s.isHardcoded(v) {
hard = true
}
}
if refs := val.Referrers(); refs != nil {
for _, ref := range *refs {
switch r := ref.(type) {
case *ssa.Call:
callStr := r.Call.Value.String()
if strings.Contains(callStr, "crypto/rand") || strings.Contains(callStr, "io.ReadFull") {
dyn = true
continue
}
if strings.Contains(callStr, "crypto/cipher") || strings.Contains(callStr, "crypto/aes") {
continue
}
if fn, ok := r.Call.Value.(*ssa.Function); ok && fn.Pkg != nil {
for i, arg := range r.Call.Args {
if arg == val {
if i < len(fn.Params) {
d, h := s.analyzeUsage(fn.Params[i])
dyn, hard = dyn || d, hard || h
}
}
}
continue
}
dyn = true
case *ssa.Slice:
d, h := s.analyzeUsage(r)
if s.isFullSlice(r) {
dyn = dyn || d
}
hard = hard || h
case *ssa.IndexAddr, *ssa.Index, *ssa.Lookup:
if vVal, ok := r.(ssa.Value); ok {
_, h := s.analyzeUsage(vVal)
hard = hard || h
}
case *ssa.UnOp:
if r.Op == token.MUL {
d, h := s.analyzeUsage(r)
dyn, hard = dyn || d, hard || h
}
case *ssa.Convert:
d, h := s.analyzeUsage(r)
dyn, hard = dyn || d, hard || h
case *ssa.Store:
if r.Addr == val {
if s.isHardcoded(r.Val) {
hard = true
} else {
dyn = true
}
}
}
}
}
if sl, ok := val.(*ssa.Slice); ok && !dyn {
if sourceRefs := sl.X.Referrers(); sourceRefs != nil {
for _, sr := range *sourceRefs {
if other, ok := sr.(*ssa.Slice); ok && other != sl {
if isSubSlice(sl, other) {
d, _ := s.analyzeUsage(other)
if d {
dyn = true
break
}
}
}
}
}
}
res := &usageResult{dyn, hard}
s.usageCache[val] = res
return dyn, hard
}
func (s *analysisState) isFullSlice(sl *ssa.Slice) bool {
l, h := getSliceRange(sl)
if l != 0 {
return false
}
if h < 0 {
return true
}
return h == s.getBufferLen(sl.X)
}
func (s *analysisState) getBufferLen(val ssa.Value) int64 {
if res, ok := s.bufferLenCache[val]; ok {
return res
}
length := GetBufferLen(val)
s.bufferLenCache[val] = length
return length
}
func isSubSlice(sub, super *ssa.Slice) bool {
l1, h1 := getSliceRange(sub)
l2, h2 := getSliceRange(super)
if l1 < 0 || l2 < 0 {
return false
}
if l2 > l1 {
return false
}
if h2 < 0 {
return true
}
if h1 < 0 {
return false
}
return h1 <= h2
}
func getSliceRange(s *ssa.Slice) (int64, int64) {
l, h, _ := GetSliceBounds(s)
return int64(l), int64(h)
}

View File

@@ -20,7 +20,6 @@ import (
"go/token"
"regexp"
"strconv"
"strings"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/buildssa"
@@ -39,8 +38,6 @@ const (
bounded
)
const maxDepth = 20
func newSliceBoundsAnalyzer(id string, description string) *analysis.Analyzer {
return &analysis.Analyzer{
Name: id,
@@ -50,12 +47,47 @@ func newSliceBoundsAnalyzer(id string, description string) *analysis.Analyzer {
}
}
func runSliceBounds(pass *analysis.Pass) (interface{}, error) {
type sliceBoundsState struct {
pass *analysis.Pass
trackCache map[trackCacheKey]*trackCacheValue
idxCache map[idxCacheKey]idxCacheValue
}
type trackCacheKey struct {
node ssa.Node
sliceCap int
}
type trackCacheValue struct {
violations []ssa.Instruction
ifs map[ssa.If]*ssa.BinOp
}
type idxCacheKey struct {
instr *ssa.IndexAddr
sliceCap int
}
type idxCacheValue struct {
val int
err error
}
func newSliceBoundsState(pass *analysis.Pass) *sliceBoundsState {
return &sliceBoundsState{
pass: pass,
trackCache: make(map[trackCacheKey]*trackCacheValue),
idxCache: make(map[idxCacheKey]idxCacheValue),
}
}
func runSliceBounds(pass *analysis.Pass) (any, error) {
ssaResult, err := getSSAResult(pass)
if err != nil {
return nil, err
}
state := newSliceBoundsState(pass)
issues := map[ssa.Instruction]*issue.Issue{}
ifs := map[ssa.If]*ssa.BinOp{}
for _, mcall := range ssaResult.SSA.SrcFuncs {
@@ -75,7 +107,7 @@ func runSliceBounds(pass *analysis.Pass) (interface{}, error) {
if slice, ok := instr.(*ssa.Slice); ok {
if _, ok := slice.X.(*ssa.Alloc); ok {
if slice.Parent() != nil {
l, h, maxIdx := extractSliceBounds(slice)
l, h, maxIdx := GetSliceBounds(slice)
violations := []ssa.Instruction{}
if maxIdx > 0 {
if !isThreeIndexSliceInsideBounds(l, h, maxIdx, sliceCap) {
@@ -87,7 +119,7 @@ func runSliceBounds(pass *analysis.Pass) (interface{}, error) {
}
}
newCap := computeSliceNewCap(l, h, maxIdx, sliceCap)
trackSliceBounds(0, newCap, slice, &violations, ifs)
state.trackSliceBounds(0, newCap, slice, &violations, ifs)
for _, s := range violations {
switch s := s.(type) {
case *ssa.Slice:
@@ -136,7 +168,7 @@ func runSliceBounds(pass *analysis.Pass) (interface{}, error) {
break
}
_, err = extractIntValueIndexAddr(instr, arrayLen)
_, err = state.extractIntValueIndexAddr(instr, arrayLen)
if err != nil {
break
}
@@ -180,7 +212,7 @@ func runSliceBounds(pass *analysis.Pass) (interface{}, error) {
}
var processBlock func(block *ssa.BasicBlock, depth int)
processBlock = func(block *ssa.BasicBlock, depth int) {
if depth == maxDepth {
if depth == MaxDepth {
return
}
depth++
@@ -194,7 +226,7 @@ func runSliceBounds(pass *analysis.Pass) (interface{}, error) {
case upperBounded:
switch tinstr := instr.(type) {
case *ssa.Slice:
_, _, m := extractSliceBounds(tinstr)
_, _, m := GetSliceBounds(tinstr)
if !isLenBound && isSliceInsideBounds(0, value, m, value) {
delete(issues, instr)
}
@@ -206,29 +238,25 @@ func runSliceBounds(pass *analysis.Pass) (interface{}, error) {
}
}
} else {
indexValue, err := extractIntValue(tinstr.Index.String())
if err != nil {
break
}
if isSliceIndexInsideBounds(value, indexValue) {
delete(issues, instr)
if indexValue, ok := GetConstantInt64(tinstr.Index); ok {
if isSliceIndexInsideBounds(value, int(indexValue)) {
delete(issues, instr)
}
}
}
}
case bounded:
switch tinstr := instr.(type) {
case *ssa.Slice:
_, _, m := extractSliceBounds(tinstr)
_, _, m := GetSliceBounds(tinstr)
if isSliceInsideBounds(value, value, m, value) {
delete(issues, instr)
}
case *ssa.IndexAddr:
indexValue, err := extractIntValue(tinstr.Index.String())
if err != nil {
break
}
if indexValue == value {
delete(issues, instr)
if indexValue, ok := GetConstantInt64(tinstr.Index); ok {
if int(indexValue) == value {
delete(issues, instr)
}
}
}
}
@@ -423,11 +451,28 @@ func decomposeIndex(v ssa.Value) (ssa.Value, int) {
}
// trackSliceBounds recursively follows slice referrers to check for index and boundary violations.
func trackSliceBounds(depth int, sliceCap int, slice ssa.Node, violations *[]ssa.Instruction, ifs map[ssa.If]*ssa.BinOp) {
if depth == maxDepth {
func (s *sliceBoundsState) trackSliceBounds(depth int, sliceCap int, slice ssa.Node, violations *[]ssa.Instruction, ifs map[ssa.If]*ssa.BinOp) {
if depth == MaxDepth {
return
}
depth++
key := trackCacheKey{slice, sliceCap}
if res, ok := s.trackCache[key]; ok {
if res == nil { // visiting
return
}
*violations = append(*violations, res.violations...)
for k, v := range res.ifs {
ifs[k] = v
}
return
}
s.trackCache[key] = nil // mark as visiting
localViolations := []ssa.Instruction{}
localIfs := make(map[ssa.If]*ssa.BinOp)
if violations == nil {
violations = &[]ssa.Instruction{}
}
@@ -436,25 +481,24 @@ func trackSliceBounds(depth int, sliceCap int, slice ssa.Node, violations *[]ssa
for _, refinstr := range *referrers {
switch refinstr := refinstr.(type) {
case *ssa.Slice:
checkAllSlicesBounds(depth, sliceCap, refinstr, violations, ifs)
s.checkAllSlicesBounds(depth, sliceCap, refinstr, &localViolations, localIfs)
switch refinstr.X.(type) {
case *ssa.Alloc, *ssa.Parameter, *ssa.Slice:
l, h, maxIdx := extractSliceBounds(refinstr)
l, h, maxIdx := GetSliceBounds(refinstr)
newCap := computeSliceNewCap(l, h, maxIdx, sliceCap)
trackSliceBounds(depth, newCap, refinstr, violations, ifs)
s.trackSliceBounds(depth, newCap, refinstr, &localViolations, localIfs)
}
case *ssa.IndexAddr:
indexValue, err := extractIntValue(refinstr.Index.String())
if err == nil && !isSliceIndexInsideBounds(sliceCap, indexValue) {
*violations = append(*violations, refinstr)
if indexValue, ok := GetConstantInt64(refinstr.Index); ok && !isSliceIndexInsideBounds(sliceCap, int(indexValue)) {
localViolations = append(localViolations, refinstr)
}
indexValue, err = extractIntValueIndexAddr(refinstr, sliceCap)
indexValue, err := s.extractIntValueIndexAddr(refinstr, sliceCap)
if err == nil && !isSliceIndexInsideBounds(sliceCap, indexValue) {
*violations = append(*violations, refinstr)
localViolations = append(localViolations, refinstr)
}
case *ssa.Call:
if ifref, cond := extractSliceIfLenCondition(refinstr); ifref != nil && cond != nil {
ifs[*ifref] = cond
localIfs[*ifref] = cond
} else {
parPos := -1
for pos, arg := range refinstr.Call.Args {
@@ -465,21 +509,46 @@ func trackSliceBounds(depth int, sliceCap int, slice ssa.Node, violations *[]ssa
if fn, ok := refinstr.Call.Value.(*ssa.Function); ok {
if len(fn.Params) > parPos && parPos > -1 {
param := fn.Params[parPos]
trackSliceBounds(depth, sliceCap, param, violations, ifs)
s.trackSliceBounds(depth, sliceCap, param, &localViolations, localIfs)
}
}
}
}
}
}
*violations = append(*violations, localViolations...)
for k, v := range localIfs {
ifs[k] = v
}
s.trackCache[key] = &trackCacheValue{localViolations, localIfs}
}
// extractIntValueIndexAddr attempts to derive a constant index value from an IndexAddr by checking its referrers.
func extractIntValueIndexAddr(refinstr *ssa.IndexAddr, sliceCap int) (int, error) {
func (s *sliceBoundsState) extractIntValueIndexAddr(refinstr *ssa.IndexAddr, sliceCap int) (int, error) {
key := idxCacheKey{refinstr, sliceCap}
if res, ok := s.idxCache[key]; ok {
return res.val, res.err
}
resVal, resErr := s.extractIntValueIndexAddrRecursive(refinstr, sliceCap)
s.idxCache[key] = idxCacheValue{resVal, resErr}
return resVal, resErr
}
func (s *sliceBoundsState) extractIntValueIndexAddrRecursive(refinstr *ssa.IndexAddr, sliceCap int) (int, error) {
base, offset := decomposeIndex(refinstr.Index)
var sliceIncr int
// Check Phi node for loop counter patterns
// Case 1: Base is a constant (e.g., s[0+3])
if val, ok := GetConstantInt64(base); ok {
finalIdx := int(val) + offset
if !isSliceIndexInsideBounds(sliceCap+sliceIncr, finalIdx) {
return finalIdx, nil
}
}
// Case 2: Base is a Phi node (loop counter)
if p, ok := base.(*ssa.Phi); ok {
var start int
var hasStart bool
@@ -519,6 +588,7 @@ func extractIntValueIndexAddr(refinstr *ssa.IndexAddr, sliceCap int) (int, error
}
for _, r := range *refs {
if bin, ok := r.(*ssa.BinOp); ok {
// Check for constant bound
bound, limit, err := extractBinOpBound(bin)
if err == nil {
incr := 0
@@ -541,6 +611,20 @@ func extractIntValueIndexAddr(refinstr *ssa.IndexAddr, sliceCap int) (int, error
return finalMaxV + offset, nil
}
}
} else if _, off, ok := extractLenBound(bin); ok {
// Check for length bound (e.g. i < len(s) + off)
// Here the limit is effectively sliceCap
limit := sliceCap
incr := -1 // extractLenBound only handles LSS for now
maxV := limit + off + incr
finalMaxV := maxV
if v == nBase && nBase != p {
finalMaxV = maxV - nOffset
}
if !isSliceIndexInsideBounds(sliceCap+sliceIncr, finalMaxV+offset) {
return finalMaxV + offset, nil
}
}
}
}
@@ -556,7 +640,7 @@ func extractIntValueIndexAddr(refinstr *ssa.IndexAddr, sliceCap int) (int, error
visited := make(map[ssa.Value]bool)
depth := 0
for len(queue) > 0 && depth < maxDepth {
for len(queue) > 0 && depth < MaxDepth {
nextQueue := []struct {
val ssa.Value
offset int
@@ -624,15 +708,15 @@ func extractIntValueIndexAddr(refinstr *ssa.IndexAddr, sliceCap int) (int, error
}
// checkAllSlicesBounds validates slice operation boundaries against the known capacity or limit.
func checkAllSlicesBounds(depth int, sliceCap int, slice *ssa.Slice, violations *[]ssa.Instruction, ifs map[ssa.If]*ssa.BinOp) {
if depth == maxDepth {
func (s *sliceBoundsState) checkAllSlicesBounds(depth int, sliceCap int, slice *ssa.Slice, violations *[]ssa.Instruction, ifs map[ssa.If]*ssa.BinOp) {
if depth == MaxDepth {
return
}
depth++
if violations == nil {
violations = &[]ssa.Instruction{}
}
sliceLow, sliceHigh, sliceMax := extractSliceBounds(slice)
sliceLow, sliceHigh, sliceMax := GetSliceBounds(slice)
if sliceMax > 0 {
if !isThreeIndexSliceInsideBounds(sliceLow, sliceHigh, sliceMax, sliceCap) {
*violations = append(*violations, slice)
@@ -644,9 +728,9 @@ func checkAllSlicesBounds(depth int, sliceCap int, slice *ssa.Slice, violations
}
switch slice.X.(type) {
case *ssa.Alloc, *ssa.Parameter, *ssa.Slice:
l, h, maxIdx := extractSliceBounds(slice)
l, h, maxIdx := GetSliceBounds(slice)
newCap := computeSliceNewCap(l, h, maxIdx, sliceCap)
trackSliceBounds(depth, newCap, slice, violations, ifs)
s.trackSliceBounds(depth, newCap, slice, violations, ifs)
}
references := slice.Referrers()
@@ -654,14 +738,14 @@ func checkAllSlicesBounds(depth int, sliceCap int, slice *ssa.Slice, violations
return
}
for _, ref := range *references {
switch s := ref.(type) {
switch r := ref.(type) {
case *ssa.Slice:
checkAllSlicesBounds(depth, sliceCap, s, violations, ifs)
switch s.X.(type) {
s.checkAllSlicesBounds(depth, sliceCap, r, violations, ifs)
switch r.X.(type) {
case *ssa.Alloc, *ssa.Parameter, *ssa.Slice:
l, h, maxIdx := extractSliceBounds(s)
l, h, maxIdx := GetSliceBounds(r)
newCap := computeSliceNewCap(l, h, maxIdx, sliceCap)
trackSliceBounds(depth, newCap, s, violations, ifs)
s.trackSliceBounds(depth, newCap, r, violations, ifs)
}
}
}
@@ -675,7 +759,7 @@ func extractSliceIfLenCondition(call *ssa.Call) (*ssa.If, *ssa.BinOp) {
refs = append(refs, *call.Referrers()...)
}
depth := 0
for len(refs) > 0 && depth < maxDepth {
for len(refs) > 0 && depth < MaxDepth {
newrefs := []ssa.Instruction{}
for _, ref := range refs {
if binop, ok := ref.(*ssa.BinOp); ok {
@@ -783,63 +867,15 @@ func isSliceIndexInsideBounds(h int, index int) bool {
return (0 <= index && index < h)
}
// isSliceInsideBounds checks if the requested slice range is within the parent slice's boundaries.
func isSliceInsideBounds(l, h int, cl, ch int) bool {
return (l <= cl && h >= ch) && (l <= ch && h >= cl)
}
// isThreeIndexSliceInsideBounds validates the boundaries and capacity of a 3-index slice (s[i:j:k]).
func isThreeIndexSliceInsideBounds(l, h, maxIdx int, oldCap int) bool {
return l >= 0 && h >= l && maxIdx >= h && maxIdx <= oldCap
}
// extractSliceBounds extracts the lower, upper, and (optional) max capacity indices from an ssa.Slice instruction.
func extractSliceBounds(slice *ssa.Slice) (int, int, int) {
var low int
if slice.Low != nil {
l, err := extractIntValue(slice.Low.String())
if err == nil {
low = l
}
}
var high int
if slice.High != nil {
h, err := extractIntValue(slice.High.String())
if err == nil {
high = h
}
}
var maxIdx int
if slice.Max != nil {
m, err := extractIntValue(slice.Max.String())
if err == nil {
maxIdx = m
}
}
return low, high, maxIdx
}
// extractIntValue attempts to parse a constant integer value from an SSA value string representation.
func extractIntValue(value string) (int, error) {
if i, err := extractIntValuePhi(value); err == nil {
return i, nil
}
parts := strings.Split(value, ":")
if len(parts) != 2 {
return 0, fmt.Errorf("invalid value: %s", value)
}
if parts[1] != "int" {
return 0, fmt.Errorf("invalid value: %s", value)
}
return strconv.Atoi(parts[0])
}
var (
sliceCapRegexp = regexp.MustCompile(`new \[(\d+)\].*`)
arrayAllocRegexp = regexp.MustCompile(`.*\[(\d+)\].*`)
)
// extractSliceCapFromAlloc parses the initial capacity of a slice from its allocation instruction string.
func extractSliceCapFromAlloc(instr string) (int, error) {
re := regexp.MustCompile(`new \[(\d+)\].*`)
var sliceCap int
matches := re.FindAllStringSubmatch(instr, -1)
matches := sliceCapRegexp.FindAllStringSubmatch(instr, -1)
if matches == nil {
return sliceCap, errors.New("no slice cap found")
}
@@ -854,30 +890,10 @@ func extractSliceCapFromAlloc(instr string) (int, error) {
return 0, errors.New("no slice cap found")
}
// extractIntValuePhi parses an integer value from an SSA Phi instruction string representation.
func extractIntValuePhi(value string) (int, error) {
re := regexp.MustCompile(`phi \[.+: (\d+):.+, .*\].*`)
var sliceCap int
matches := re.FindAllStringSubmatch(value, -1)
if matches == nil {
return sliceCap, fmt.Errorf("invalid value: %s", value)
}
if len(matches) > 0 {
m := matches[0]
if len(m) > 1 {
return strconv.Atoi(m[1])
}
}
return 0, fmt.Errorf("invalid value: %s", value)
}
// extractArrayAllocValue parses the constant length of an array allocation from its type string.
func extractArrayAllocValue(value string) (int, error) {
re := regexp.MustCompile(`.*\[(\d+)\].*`)
var sliceCap int
matches := re.FindAllStringSubmatch(value, -1)
matches := arrayAllocRegexp.FindAllStringSubmatch(value, -1)
if matches == nil {
return sliceCap, fmt.Errorf("invalid value: %s", value)
}

View File

@@ -16,21 +16,39 @@ package analyzers
import (
"fmt"
"go/constant"
"go/token"
"go/types"
"log"
"math"
"os"
"regexp"
"strconv"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/buildssa"
"golang.org/x/tools/go/ssa"
"github.com/securego/gosec/v2/issue"
)
// isSliceInsideBounds checks if the requested slice range is within the parent slice's boundaries.
func isSliceInsideBounds(l, h int, cl, ch int) bool {
return (l <= cl && h >= ch) && (l <= ch && h >= cl)
}
// isThreeIndexSliceInsideBounds validates the boundaries and capacity of a 3-index slice (s[i:j:k]).
func isThreeIndexSliceInsideBounds(l, h, maxIdx int, oldCap int) bool {
return l >= 0 && h >= l && maxIdx >= h && maxIdx <= oldCap
}
// MaxDepth defines the maximum recursion depth for SSA analysis to avoid infinite loops and memory exhaustion.
const MaxDepth = 20
// SSAAnalyzerResult contains various information returned by the
// SSA analysis along with some configuration
type SSAAnalyzerResult struct {
Config map[string]interface{}
Config map[string]any
Logger *log.Logger
SSA *buildssa.SSA
}
@@ -102,3 +120,184 @@ func issueCodeSnippet(fileSet *token.FileSet, pos token.Pos) string {
}
return code
}
// IntTypeInfo represents integer type properties
type IntTypeInfo struct {
Signed bool
Size int
Min int
Max uint
}
var intTypeRegexp = regexp.MustCompile(`^(?P<type>u?int)(?P<size>\d{1,2})?$`)
// ParseIntType parses an integer type string into IntTypeInfo
func ParseIntType(intType string) (IntTypeInfo, error) {
matches := intTypeRegexp.FindStringSubmatch(intType)
if matches == nil {
return IntTypeInfo{}, fmt.Errorf("no integer type match found for %s", intType)
}
it := matches[intTypeRegexp.SubexpIndex("type")]
is := matches[intTypeRegexp.SubexpIndex("size")]
signed := it == "int"
intSize := strconv.IntSize
if is != "" {
var err error
intSize, err = strconv.Atoi(is)
if err != nil {
return IntTypeInfo{}, fmt.Errorf("failed to parse the integer type size: %w", err)
}
}
if intSize != 8 && intSize != 16 && intSize != 32 && intSize != 64 && is != "" {
return IntTypeInfo{}, fmt.Errorf("invalid bit size: %d", intSize)
}
var minVal int
var maxVal uint
if signed {
switch intSize {
case 8:
minVal = math.MinInt8
maxVal = math.MaxInt8
case 16:
minVal = math.MinInt16
maxVal = math.MaxInt16
case 32:
minVal = math.MinInt32
maxVal = math.MaxInt32
case 64:
minVal = math.MinInt64
// We are on 64-bit architecture where uint is 64-bit
maxVal = uint(math.MaxInt64)
default:
return IntTypeInfo{}, fmt.Errorf("unsupported bit size: %d", intSize)
}
} else {
minVal = 0
switch intSize {
case 8:
maxVal = math.MaxUint8
case 16:
maxVal = math.MaxUint16
case 32:
maxVal = math.MaxUint32
case 64:
// We are on 64-bit architecture where uint is 64-bit
maxVal = uint(math.MaxUint64)
default:
return IntTypeInfo{}, fmt.Errorf("unsupported bit size: %d", intSize)
}
}
return IntTypeInfo{
Signed: signed,
Size: intSize,
Min: minVal,
Max: maxVal,
}, nil
}
// GetConstantInt64 extracts a constant int64 value from an ssa.Value
func GetConstantInt64(v ssa.Value) (int64, bool) {
if c, ok := v.(*ssa.Const); ok {
if c.Value != nil {
if val, ok := constant.Int64Val(c.Value); ok {
return val, true
}
}
}
if unOp, ok := v.(*ssa.UnOp); ok && unOp.Op == token.SUB {
if val, ok := GetConstantInt64(unOp.X); ok {
return -val, true
}
}
return 0, false
}
// GetConstantUint64 extracts a constant uint64 value from an ssa.Value
func GetConstantUint64(v ssa.Value) (uint64, bool) {
if c, ok := v.(*ssa.Const); ok {
if c.Value != nil {
if val, ok := constant.Uint64Val(c.Value); ok {
return val, true
}
}
}
return 0, false
}
// GetSliceBounds extracts low, high, and max indices from a slice instruction
func GetSliceBounds(s *ssa.Slice) (int, int, int) {
var low, high, maxIdx int
if s.Low != nil {
if val, ok := GetConstantInt64(s.Low); ok {
low = int(val)
}
}
if s.High != nil {
if val, ok := GetConstantInt64(s.High); ok {
high = int(val)
}
}
if s.Max != nil {
if val, ok := GetConstantInt64(s.Max); ok {
maxIdx = int(val)
}
}
return low, high, maxIdx
}
// GetBufferLen attempts to find the constant length of a buffer/slice/array
func GetBufferLen(val ssa.Value) int64 {
current := val
for {
t := current.Type()
if ptr, ok := t.Underlying().(*types.Pointer); ok {
t = ptr.Elem().Underlying()
}
if arr, ok := t.(*types.Array); ok {
return arr.Len()
}
if sl, ok := current.(*ssa.Slice); ok {
current = sl.X
continue
}
break
}
return -1
}
// BuildCallerMap builds a map of function names to their call sites
func BuildCallerMap(funcs []*ssa.Function) map[string][]*ssa.Call {
callerMap := make(map[string][]*ssa.Call)
for _, f := range funcs {
for _, b := range f.Blocks {
for _, i := range b.Instrs {
if c, ok := i.(*ssa.Call); ok {
var name string
if c.Call.Method != nil {
name = c.Call.Method.FullName()
} else {
name = c.Call.Value.String()
}
callerMap[name] = append(callerMap[name], c)
}
}
}
}
return callerMap
}
// toUint64 casts int64 to uint64 preserving the bit pattern (2's complement) and suppresses the linter warning.
func toUint64(i int64) uint64 {
return uint64(i) // #nosec
}
// toInt64 casts uint64 to int64 preserving the bit pattern and suppresses the linter warning.
func toInt64(u uint64) int64 {
return int64(u) // #nosec
}

10
go.mod
View File

@@ -7,16 +7,16 @@ require (
github.com/gookit/color v1.6.0
github.com/lib/pq v1.10.9
github.com/mozilla/tls-observatory v0.0.0-20250923143331-eef96233227e
github.com/onsi/ginkgo/v2 v2.27.3
github.com/onsi/gomega v1.38.3
github.com/openai/openai-go/v3 v3.15.0
github.com/onsi/ginkgo/v2 v2.27.4
github.com/onsi/gomega v1.39.0
github.com/openai/openai-go/v3 v3.16.0
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
github.com/stretchr/testify v1.11.1
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/crypto v0.46.0
golang.org/x/text v0.32.0
golang.org/x/text v0.33.0
golang.org/x/tools v0.40.0
google.golang.org/genai v1.40.0
google.golang.org/genai v1.41.0
)
require (

20
go.sum
View File

@@ -306,13 +306,13 @@ github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXW
github.com/olekukonko/tablewriter v0.0.2/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo/v2 v2.27.3 h1:ICsZJ8JoYafeXFFlFAG75a7CxMsJHwgKwtO+82SE9L8=
github.com/onsi/ginkgo/v2 v2.27.3/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/ginkgo/v2 v2.27.4 h1:fcEcQW/A++6aZAZQNUmNjvA9PSOzefMJBerHJ4t8v8Y=
github.com/onsi/ginkgo/v2 v2.27.4/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM=
github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4=
github.com/openai/openai-go/v3 v3.15.0 h1:hk99rM7YPz+M99/5B/zOQcVwFRLLMdprVGx1vaZ8XMo=
github.com/openai/openai-go/v3 v3.15.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo=
github.com/onsi/gomega v1.39.0 h1:y2ROC3hKFmQZJNFeGAMeHZKkjBL65mIZcvrLQBF9k6Q=
github.com/onsi/gomega v1.39.0/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4=
github.com/openai/openai-go/v3 v3.16.0 h1:VdqS+GFZgAvEOBcWNyvLVwPlYEIboW5xwiUCcLrVf8c=
github.com/openai/openai-go/v3 v3.16.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
@@ -580,8 +580,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -663,8 +663,8 @@ google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww
google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genai v1.40.0 h1:kYxyQSH+vsib8dvsgyLJzsVEIv5k3ZmHJyVqdvGncmc=
google.golang.org/genai v1.40.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk=
google.golang.org/genai v1.41.0 h1:ayXl75LjTmqTu0y94yr96d17gIb4zF8gWVzX2TgioEY=
google.golang.org/genai v1.41.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk=
google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181107211654-5fc9ac540362/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=

View File

@@ -62,6 +62,11 @@ var sqlCallIdents = map[string]map[string]int{
},
}
var (
sqlRegexp = regexp.MustCompile("(?i)(SELECT|DELETE|INSERT|UPDATE|INTO|FROM|WHERE)( |\n|\r|\t)")
sqlFormatRegexp = regexp.MustCompile("%[^bdoxXfFp]")
)
// 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)
@@ -294,7 +299,7 @@ func NewSQLStrConcat(id string, _ gosec.Config) (gosec.Rule, []ast.Node) {
rule := &sqlStrConcat{
sqlStatement: sqlStatement{
patterns: []*regexp.Regexp{
regexp.MustCompile("(?i)(SELECT|DELETE|INSERT|UPDATE|INTO|FROM|WHERE)( |\n|\r|\t)"),
sqlRegexp,
},
MetaData: issue.NewMetaData(id, "SQL string concatenation", issue.Medium, issue.High),
CallList: gosec.NewCallList(),
@@ -454,8 +459,8 @@ func NewSQLStrFormat(id string, _ gosec.Config) (gosec.Rule, []ast.Node) {
noIssueQuoted: gosec.NewCallList(),
sqlStatement: sqlStatement{
patterns: []*regexp.Regexp{
regexp.MustCompile("(?i)(SELECT|DELETE|INSERT|UPDATE|INTO|FROM|WHERE)( |\n|\r|\t)"),
regexp.MustCompile("%[^bdoxXfFp]"),
sqlRegexp,
sqlFormatRegexp,
},
MetaData: issue.NewMetaData(id, "SQL string formatting", issue.Medium, issue.High),
},

View File

@@ -862,4 +862,790 @@ func main() {
}
`,
}, 0, gosec.NewConfig()},
{[]string{
`
package main
func sneakyNEQ(a int) uint {
if a == 3 || a != 4 {
return uint(a)
}
panic("not supported")
}
`,
}, 1, gosec.NewConfig()},
{[]string{
`
package main
func checkThenArithmetic(a int) uint {
if a >= 0 && a < 10 {
return uint(a + 1)
}
panic("not supported")
}
`,
}, 0, gosec.NewConfig()},
{[]string{
`
package main
func binaryTruncation(a int) uint16 {
return uint16(a & 0xffff)
}
`,
}, 0, gosec.NewConfig()},
{[]string{
`
package main
func builtinMin(a, b int) uint16 {
if a < 0 || a > 100 || b < 0 || b > 100 {
return 0
}
result := min(a, b)
return uint16(result)
}
`,
}, 0, gosec.NewConfig()},
{[]string{
`
package main
func loopIndices(myArr []string) {
for i, _ := range myArr {
_ = uint64(i)
}
for i := 0; i < 10; i++ {
_ = uint64(i)
}
}
`,
}, 0, gosec.NewConfig()},
{[]string{
`
package main
func bitShifting(u32 uint32) uint8 {
return uint8(u32 >> 24)
}
`,
}, 0, gosec.NewConfig()},
{[]string{
`
package main
import "time"
func unixMilli() uint64 {
return uint64(time.Now().UnixMilli())
}
`,
}, 0, gosec.NewConfig()},
{[]string{
`
package main
import "math"
type innerStruct struct {
u32 *uint32
}
type nestedStruct struct {
i *innerStruct
}
func nestedPointerCheck(n nestedStruct) {
if *n.i.u32 > math.MaxInt32 {
panic("out of range")
} else {
i32 := int32(*n.i.u32)
_ = i32
}
}
`,
}, 0, gosec.NewConfig()},
{[]string{
`
package main
func f(_ uint64) {}
func nestedSwitch(x int32) {
switch {
case x > 0:
switch {
case true:
f(uint64(x))
}
}
}
`,
}, 0, gosec.NewConfig()},
{[]string{
`
package main
func constantArithmetic(someLen int) {
const multiple = 4
_ = uint8(multiple - (int(someLen) % multiple))
}
`,
}, 0, gosec.NewConfig()},
{[]string{
`
package main
import "fmt"
func main() {
x := int64(-1)
y := uint64(x)
fmt.Println(y)
}
`,
}, 1, gosec.NewConfig()},
{[]string{
`
package main
import "math"
func main() {
u := uint64(math.MaxUint64)
i := int64(u)
_ = i
}
`,
}, 1, gosec.NewConfig()},
{[]string{`
package main
func checkGEQ(x int) uint64 {
if x >= 10 {
return uint64(x)
}
return 0
}
func checkGTR(x int) uint64 {
if x > 10 {
return uint64(x)
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func checkNEQ(x int) uint64 {
if x != 10 {
return 0
}
// x == 10 here
return uint64(x)
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func addProp(x uint8) uint16 {
// x is 0..255. y = x + 10 is 10..265.
return uint16(x + 10)
}
func subProp(x uint8) uint16 {
y := int(x)
if y > 20 && y < 100 {
return uint16(y - 10)
}
return 0
}
func subFlipped(x int) uint16 {
if x > 0 && x < 10 {
return uint16(20 - x)
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func andOp(x int) uint16 {
return uint16(x & 0xFF)
}
func shrOp(x int) uint16 {
if x >= 0 && x <= 0xFFFF {
y := uint16(x)
return uint16(y >> 4)
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
import "strconv"
func parseVariants(s string) {
v8, _ := strconv.ParseInt(s, 10, 8)
_ = int8(v8)
v64, _ := strconv.ParseInt(s, 10, 64)
_ = int64(v64)
u32, _ := strconv.ParseUint(s, 10, 32)
_ = uint32(u32)
u64, _ := strconv.ParseUint(s, 10, 64)
_ = uint64(u64)
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func remOp(x int) uint16 {
y := x % 10
if y >= 0 {
return uint16(y)
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func negProp(y int) uint16 {
if y > -10 && y < 0 {
x := -y
return uint16(x)
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func minMaxProp(a, b int) uint16 {
if a > 0 && a < 10 && b > 0 && b < 20 {
x := min(a, b)
y := max(a, b)
return uint16(x + y)
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func subFlippedBound(y int) uint16 {
if (100 - y) > 0 && (100 - y) < 50 {
return uint16(100 - y)
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func remSigned(y int) uint16 {
x := y % 10 // range -9..9
if x >= 0 {
return uint16(x)
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func bitwiseProp(y int) uint16 {
if (y & 0xFF) < 100 {
return uint16(y & 0xFF)
}
return 0
}
func shiftProp(y uint16) uint8 {
if (y >> 4) < 10 {
return uint8(y >> 4)
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
import "strconv"
func parse64(s string) uint32 {
v, _ := strconv.ParseUint(s, 10, 64)
if v < 1000 {
return uint32(v)
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func addPropRel(x int) uint16 {
if (x + 10) < 100 && (x + 10) > 0 {
return uint16(x + 10)
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func negExplicit(y int) uint16 {
if y > -10 && y < -5 {
x := -y
return uint16(x)
}
return 0
}
func subFlippedExplicit(y int) uint16 {
if y > 60 && y < 90 {
return uint16(100 - y)
}
return 0
}
func addExplicit(y int) uint16 {
if y > 10 && y < 20 {
return uint16(y + 100)
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func minMaxCheck(a, b int) uint16 {
if a > 0 && a < 10 && b > 10 && b < 20 {
return uint16(min(a, b) + max(a, b))
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
import "strconv"
func parseExplicit(s string) {
v, _ := strconv.ParseInt(s, 10, 64)
if v > 0 && v < 100 {
_ = uint8(v)
}
u, _ := strconv.ParseUint(s, 10, 64)
if u < 100 {
_ = uint8(u)
}
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func remExplicit(y int) uint16 {
x := y % 10
if x >= 0 && x < 10 {
return uint16(x)
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func andPropCheck(x int) uint8 {
if x > 1000 {
return uint8(x & 0x7F) // x & 0x7F is [0, 127]
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func shrPropCheck(x int) uint8 {
if x > 0 && x < 4000 {
return uint8(x >> 4) // 4000 >> 4 = 250
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func remPropCheck(x int) uint8 {
if x > -100 {
y := x % 10 // range [-9, 9]
if y >= 0 {
return uint8(y)
}
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func shrFallback(x uint16) uint8 {
return uint8(x >> 8) // computeRange fallback: uint16.Max >> 8 = 255 (fits uint8)
}
func remSignedFallback(x int) int8 {
return int8(x % 10) // computeRange fallback: [-9, 9] fits int8
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func shrPropComplex(x int) uint8 {
if x > 0 && x < 1000 {
y := x >> 2 // y is [0, 250]
return uint8(y)
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func remPropComplex(x int) int8 {
if x > -100 && x < 100 {
y := x % 10 // y is [-9, 9]
return int8(y)
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func mulProp(x int) uint8 {
if x >= 0 && x < 20 {
return uint8(x * 10) // [0, 190] -> fits in uint8 (255)
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func quoProp(x int) uint8 {
if x >= 0 && x < 2000 {
return uint8(x / 10) // [0, 199] -> fits in uint8
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func mulProp(x int) int8 {
if x < 0 && x > -10 {
return int8(x * 10) // [-100, 0] -> fits in int8
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func quoProp(x int) int8 {
if x < 0 && x > -1000 {
return int8(x / 10) // [-99, 0] -> fits in int8
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func mulOverflow(x int) uint8 {
if x >= 0 && x < 30 {
return uint8(x * 10) // [10, 290] -> overflows uint8
}
return 0
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main
func mulProp(x int) uint8 {
if x < 0 && x > -10 {
return uint8(x * 10) // [-90, 0] -> negative
}
return 0
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main
func quoProp(x int) uint8 {
if x < 0 && x > -1000 {
return uint8(x / 10) // [-99, 0] -> negative
}
return 0
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main
func quoNegProp(x int) uint8 {
if x > -100 && x < -10 {
return uint8(x / -5) // [-99, -11] / -5 -> [2, 19] -> fits in uint8
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func mulNegProp(x int) uint8 {
if x > -10 && x < 0 {
return uint8(x * -5) // [-9, -1] * -5 -> [5, 45] -> fits in uint8
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func coverageProp(x int) {
// SUB val - x
{
a := 10
b := 100 - a // 90
_ = int8(b)
}
// MUL neg defined
{
a := 10
b := a * -5 // -50
_ = int8(b)
}
// QUO neg defined
{
a := 100
b := a / -2 // -50
_ = int8(b)
}
// REM neg
{
a := -50
b := a % 10
_ = int8(b)
}
// Square (isSameOrRelated)
{
a := 10
b := a * a // 100
_ = int8(b)
}
_ = x
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func shrProp(x uint8) uint8 {
return x >> 1
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func shlProp(x uint64) uint16 {
if x < 256 {
return uint16(x << 8) // max 255 << 8 = 65280. Fits in uint16 (65535)
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func shlOverflow(x uint64) uint16 {
if x < 256 {
return uint16(x << 9) // max 255 << 9 = 130560. Overflows uint16.
}
return 0
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main
func shlSafeCheck(x int) uint16 {
if x > 0 && x < 10 {
return uint16(x << 4) // max 9 << 4 = 144. Fits.
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func shlUnsafeCheck(x int) uint16 {
if x > 0 && x < 10000 {
return uint16(x << 4) // max 9999 << 4 = 159984. Overflows uint16.
}
return 0
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main
func shlCompute(x int) uint8 {
// x & 0x0F -> range [0, 15]
// 15 << 2 = 60. Fits in uint8.
return uint8((x & 0x0F) << 2)
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func remUint(x uint) uint8 {
// x is uint (non-negative).
// x % 10 -> range [0, 9].
// Fits in uint8.
return uint8(x % 10)
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func shlCondition(x int) uint8 {
// if x << 2 < 100
// x range is inferred.
// x*4 < 100 => x < 25.
// uint8(x) is safe.
if (x << 2) < 100 && x >= 0 {
return uint8(x)
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func shlMinUpdate(x int) uint8 {
// x > 10 -> x in [11, Max]
// x << 2 -> [44, Max]
if x > 10 && x < 20 {
return uint8(x << 2) // [44, 76] fits uint8
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
type S struct { F int }
func fieldCompareRHS(s *S) uint8 {
// 10 < s.F -> s.F > 10
// s.F is struct field, different SSA reads.
if 10 < s.F && s.F < 250 {
return uint8(s.F)
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func rhsOpFallback(x int) uint8 {
// 100 > x << 2 => x << 2 < 100 => x < 25
if 100 > x << 2 && x >= 0 {
return uint8(x)
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func inverseAddSafe(x int) uint8 {
// x + 1000 < 1010 => x < 10
// If we miss inverse op, we see x < 1010 (unsafe)
if x + 1000 < 1010 && x >= 0 {
return uint8(x) // Safe
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func inverseSubUnsafe(x int) uint8 {
// x - 1000 < 10 => x < 1010
// If we miss inverse op, we see x < 10 (safe)
// Actually unsafe.
if x - 1000 < 10 && x >= 0 {
return uint8(x) // Unsafe
}
return 0
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main
func inverseShrSafe(x int) uint8 {
// x >> 2 < 10 => x < 40 (approx 10 << 2)
// Actually [0, 39] >> 2 is [0, 9]. 40 >> 2 is 10.
// So distinct x < 40.
if x >> 2 < 10 && x >= 0 {
return uint8(x) // Safe
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func inverseMulSafe(x int) uint8 {
// x * 10 < 100 => x < 10
if x * 10 < 100 && x >= 0 {
return uint8(x) // Safe
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func mulMinUpdate(x int) uint8 {
// x > 10. x * 2 > 20.
// if x < 50. x * 2 < 100.
// result [22, 100]. Fits uint8.
// Hits MUL minValue update (recursive tightens forward).
if x > 10 && x < 50 {
return uint8(x * 2)
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func quoMinUpdate(x int) uint8 {
// x > 20. x / 2 > 10.
// x < 100. x / 2 < 50.
// result [10, 50]. Fits uint8.
// Hits QUO minValue update.
if x > 20 && x < 100 {
return uint8(x / 2)
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func mulOverflow64(x uint64) uint8 {
if x >= 1 && x <= 2 {
return uint8(x * 0x8000000000000001)
}
return 0
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main
type T int64
func testChangeType(x T) int8 {
if x > 0 && x < 100 {
return int8(x) // Propagate through ChangeType (T is int64-based)
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func testCommutativeAdd(x int) uint8 {
if 10 + x < 30 && x > 0 {
return uint8(x) // Safe [1, 19]
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func testXOR(x uint8) int8 {
if x < 128 {
y := ^x // [0, 127] -> [128, 255]
return int8(y) // Unsafe
}
return 0
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main
func testInvFlippedQuo(x int) uint16 {
if x > 0 && 10000 / x < 5 {
return uint16(x) // Unsafe: x > 2000.
}
return 0
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main
func testInvQuo(x int64) uint8 {
if x > 0 && x / 10 < 5 {
return uint8(x) // Safe: x < 50
}
return 0
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func testDoubleReturn(x int) (uint8, uint16) {
if x > 0 && x < 10 {
return uint8(x), uint16(x)
}
return 0, 0
}
`}, 0, gosec.NewConfig()},
}

View File

@@ -493,4 +493,220 @@ func main() {
}
`}, 1, gosec.NewConfig()},
{[]string{`package main
import (
"crypto/aes"
"crypto/cipher"
)
func Decrypt(data []byte, key []byte) ([]byte, error) {
block, _ := aes.NewCipher(key)
gcm, _ := cipher.NewGCM(block)
nonceSize := gcm.NonceSize()
if len(data) < nonceSize {
return nil, nil
}
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
return gcm.Open(nil, nonce, ciphertext, nil)
}
func main() {}
`}, 0, gosec.NewConfig()},
{[]string{`package main
import (
"crypto/aes"
"crypto/cipher"
)
const iv = "1234567812345678"
func wrapper(s string, b cipher.Block) {
cipher.NewCTR(b, []byte(s))
}
func main() {
b, _ := aes.NewCipher([]byte("1234567812345678"))
wrapper(iv, b)
}
`}, 1, gosec.NewConfig()},
{[]string{`package main
import (
"crypto/aes"
"crypto/cipher"
)
var globalIV = []byte("1234567812345678")
func wrapper(iv []byte, b cipher.Block) {
cipher.NewCTR(b, iv)
}
func main() {
b, _ := aes.NewCipher([]byte("1234567812345678"))
wrapper(globalIV, b)
}
`}, 1, gosec.NewConfig()},
{[]string{`package main
import (
"crypto/cipher"
)
func recursive(s string, b cipher.Block) {
recursive(s, b)
cipher.NewCTR(b, []byte(s))
}
func main() {
recursive("1234567812345678", nil)
}
`}, 1, gosec.NewConfig()},
{[]string{`package main
import (
"crypto/aes"
"crypto/cipher"
)
func main() {
k := make([]byte, 48)
key, iv := k[:32], k[32:]
block, _ := aes.NewCipher(key)
_ = cipher.NewCTR(block, iv)
}
`}, 1, gosec.NewConfig()},
{[]string{`package main
import (
"crypto/aes"
"crypto/cipher"
)
func main() {
k := make([]byte, 48)
k[32] = 1
key, iv := k[:32], k[32:]
block, _ := aes.NewCipher(key)
_ = cipher.NewCTR(block, iv)
}
`}, 1, gosec.NewConfig()},
{[]string{`package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
)
func main() {
iv := make([]byte, 16)
rand.Read(iv)
block, _ := aes.NewCipher([]byte("12345678123456781234567812345678"))
_ = cipher.NewCTR(block, iv)
}
`}, 0, gosec.NewConfig()},
{[]string{`package main
import (
"crypto/aes"
"crypto/cipher"
"io"
)
func main() {
iv := make([]byte, 16)
io.ReadFull(nil, iv)
block, _ := aes.NewCipher([]byte("12345678123456781234567812345678"))
_ = cipher.NewCTR(block, iv)
}
`}, 0, gosec.NewConfig()},
{[]string{`package main
import (
"crypto/aes"
"crypto/cipher"
)
func fill(b []byte) {
b[0] = 1
}
func main() {
iv := make([]byte, 16)
fill(iv)
block, _ := aes.NewCipher([]byte("12345678123456781234567812345678"))
_ = cipher.NewCTR(block, iv)
}
`}, 1, gosec.NewConfig()},
{[]string{`package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
)
func main() {
iv := make([]byte, 16)
rand.Read(iv)
iv[0] = 1 // overwriting
block, _ := aes.NewCipher([]byte("12345678123456781234567812345678"))
_ = cipher.NewCTR(block, iv)
}
`}, 1, gosec.NewConfig()},
{[]string{`package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
)
func main() {
iv := make([]byte, 16)
rand.Read(iv[0:8])
block, _ := aes.NewCipher([]byte("12345678123456781234567812345678"))
_ = cipher.NewCTR(block, iv)
}
`}, 1, gosec.NewConfig()},
{[]string{`package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
)
func main() {
iv := make([]byte, 16)
rand.Read(iv[0:16])
block, _ := aes.NewCipher([]byte("12345678123456781234567812345678"))
_ = cipher.NewCTR(block, iv)
}
`}, 0, gosec.NewConfig()},
{[]string{`package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
)
func main() {
buf := make([]byte, 128)
rand.Read(buf[32:48])
block, _ := aes.NewCipher([]byte("12345678123456781234567812345678"))
_ = cipher.NewCTR(block, buf[32:48])
}
`}, 0, gosec.NewConfig()},
}

View File

@@ -588,10 +588,67 @@ import "fmt"
func main() {
s := make([]byte, 2)
for i := range 3 {
for i := 0; i < 3; i++ {
x := s[i+2]
fmt.Println(x)
}
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main
import "fmt"
func main() {
s := make([]byte, 2)
i := 0
// decomposeIndex should handle i + 1 + 2 = i + 3
fmt.Println(s[i+1+2])
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main
import "fmt"
func main() {
s := make([]byte, 5)
for i := 0; i+1 < len(s); i++ {
// i+1 < 5 => i < 4. Max i = 3. i+1 = 4. s[4] is safe.
fmt.Println(s[i+1])
}
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
import "fmt"
func main() {
var a [10]int
idx := 12
fmt.Println(a[idx])
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main
import "fmt"
func main() {
s := make([]byte, 4)
if 5 < len(s) {
fmt.Println(s[4])
}
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func main() {
var a [10]int
k := 11
_ = a[:5:k]
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main
import "fmt"
func main() {
s := make([]int, 5)
idx := -1
fmt.Println(s[idx])
}
`}, 1, gosec.NewConfig()},
}