mirror of
https://github.com/securego/gosec.git
synced 2026-01-15 01:33:41 +08:00
Compare commits
3 Commits
7387d22592
...
833d7919e0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
833d7919e0 | ||
|
|
0cc9e01a9d | ||
|
|
303f84d111 |
187
analyzers/bench_test.go
Normal file
187
analyzers/bench_test.go
Normal 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
@@ -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))
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
10
go.mod
@@ -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
20
go.sum
@@ -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=
|
||||
|
||||
11
rules/sql.go
11
rules/sql.go
@@ -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),
|
||||
},
|
||||
|
||||
@@ -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()},
|
||||
}
|
||||
|
||||
@@ -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()},
|
||||
}
|
||||
|
||||
@@ -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()},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user