mirror of
https://github.com/securego/gosec.git
synced 2026-06-20 00:15:59 +02:00
* fix(taint): gate *http.Request auto-taint on entry-point detection (#1629) isParameterTainted unconditionally tainted any *http.Request parameter by type, even when the function had known callers passing constant-URL requests. Check the CHA call graph first: only auto-taint when the function has no in-edges (true external entry point). When callers exist, fall through to the existing caller-verification loop instead. Fixes #1629 * Address Barry AI Security Analysis * improve code coverage * fix lint * taint mechanism, framework agnostic * address lint warning
1138 lines
35 KiB
Go
1138 lines
35 KiB
Go
package taint
|
|
|
|
import (
|
|
"fmt"
|
|
"go/ast"
|
|
"go/constant"
|
|
"go/parser"
|
|
"go/token"
|
|
"go/types"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/tools/go/analysis"
|
|
"golang.org/x/tools/go/analysis/passes/buildssa"
|
|
"golang.org/x/tools/go/callgraph/cha"
|
|
"golang.org/x/tools/go/ssa"
|
|
|
|
"github.com/securego/gosec/v2/internal/ssautil"
|
|
"github.com/securego/gosec/v2/issue"
|
|
)
|
|
|
|
func TestMakeAnalyzerRunnerReturnsErrorWithoutSSA(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
rule := &RuleInfo{ID: "T001", Description: "desc", Severity: "HIGH"}
|
|
runner := makeAnalyzerRunner(rule, &Config{})
|
|
|
|
pass := &analysis.Pass{ResultOf: map[*analysis.Analyzer]interface{}{}}
|
|
if _, err := runner(pass); err == nil {
|
|
t.Fatalf("expected error when SSA result is missing")
|
|
}
|
|
}
|
|
|
|
func TestMakeAnalyzerRunnerReturnsNilWhenNoSourceFunctions(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
rule := &RuleInfo{ID: "T001", Description: "desc", Severity: "HIGH"}
|
|
runner := makeAnalyzerRunner(rule, &Config{})
|
|
|
|
pass := &analysis.Pass{
|
|
ResultOf: map[*analysis.Analyzer]interface{}{
|
|
buildssa.Analyzer: &ssautil.SSAAnalyzerResult{SSA: &buildssa.SSA{}},
|
|
},
|
|
}
|
|
|
|
got, err := runner(pass)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got != nil {
|
|
t.Fatalf("expected nil result when no source functions exist")
|
|
}
|
|
}
|
|
|
|
func TestNewIssuePopulatesFields(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tempDir := t.TempDir()
|
|
filePath := filepath.Join(tempDir, "main.go")
|
|
src := "package main\n\nfunc main() {\n\tprintln(\"hello\")\n}\n"
|
|
if err := os.WriteFile(filePath, []byte(src), 0o600); err != nil {
|
|
t.Fatalf("failed to write temp source: %v", err)
|
|
}
|
|
|
|
fset := token.NewFileSet()
|
|
parsed, err := parser.ParseFile(fset, filePath, src, 0)
|
|
if err != nil {
|
|
t.Fatalf("failed to parse source: %v", err)
|
|
}
|
|
|
|
iss := newIssue("T001", "taint finding", fset, parsed.Package, issue.High, issue.High)
|
|
if iss.RuleID != "T001" {
|
|
t.Fatalf("unexpected rule id: %s", iss.RuleID)
|
|
}
|
|
if iss.File != filePath {
|
|
t.Fatalf("unexpected file path: %s", iss.File)
|
|
}
|
|
if iss.Line != "1" || iss.Col != "1" {
|
|
t.Fatalf("unexpected location: line=%s col=%s", iss.Line, iss.Col)
|
|
}
|
|
if iss.What != "taint finding" {
|
|
t.Fatalf("unexpected description: %s", iss.What)
|
|
}
|
|
}
|
|
|
|
func TestIssueCodeSnippetReadsSource(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tempDir := t.TempDir()
|
|
filePath := filepath.Join(tempDir, "snippet.go")
|
|
src := "package main\n\nfunc main() {\n\tprintln(\"hello\")\n}\n"
|
|
if err := os.WriteFile(filePath, []byte(src), 0o600); err != nil {
|
|
t.Fatalf("failed to write temp source: %v", err)
|
|
}
|
|
|
|
fset := token.NewFileSet()
|
|
parsed, err := parser.ParseFile(fset, filePath, src, 0)
|
|
if err != nil {
|
|
t.Fatalf("failed to parse source: %v", err)
|
|
}
|
|
|
|
snippet := issueCodeSnippet(fset, parsed.Package)
|
|
if snippet == "" {
|
|
t.Fatalf("expected non-empty snippet")
|
|
}
|
|
}
|
|
|
|
func TestIsContextTypeWithContextContext(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Build a context.Context named type matching the real context package.
|
|
pkg := types.NewPackage("context", "context")
|
|
iface := types.NewInterfaceType(nil, nil)
|
|
obj := types.NewTypeName(token.NoPos, pkg, "Context", nil)
|
|
named := types.NewNamed(obj, iface, nil)
|
|
|
|
if !isContextType(named) {
|
|
t.Fatalf("expected isContextType to return true for context.Context")
|
|
}
|
|
}
|
|
|
|
func TestIsContextTypeWithPointerToContextContext(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
pkg := types.NewPackage("context", "context")
|
|
iface := types.NewInterfaceType(nil, nil)
|
|
obj := types.NewTypeName(token.NoPos, pkg, "Context", nil)
|
|
named := types.NewNamed(obj, iface, nil)
|
|
ptr := types.NewPointer(named)
|
|
|
|
if !isContextType(ptr) {
|
|
t.Fatalf("expected isContextType to return true for *context.Context")
|
|
}
|
|
}
|
|
|
|
func TestIsContextTypeRejectsNonContextTypes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := []struct {
|
|
name string
|
|
typ types.Type
|
|
}{
|
|
{
|
|
name: "http.Request",
|
|
typ: func() types.Type {
|
|
pkg := types.NewPackage("net/http", "http")
|
|
obj := types.NewTypeName(token.NoPos, pkg, "Request", nil)
|
|
return types.NewNamed(obj, types.NewStruct(nil, nil), nil)
|
|
}(),
|
|
},
|
|
{
|
|
name: "string",
|
|
typ: types.Typ[types.String],
|
|
},
|
|
{
|
|
name: "wrong package same name",
|
|
typ: func() types.Type {
|
|
pkg := types.NewPackage("myapp/context", "context")
|
|
obj := types.NewTypeName(token.NoPos, pkg, "Context", nil)
|
|
return types.NewNamed(obj, types.NewInterfaceType(nil, nil), nil)
|
|
}(),
|
|
},
|
|
{
|
|
name: "context package wrong name",
|
|
typ: func() types.Type {
|
|
pkg := types.NewPackage("context", "context")
|
|
obj := types.NewTypeName(token.NoPos, pkg, "CancelFunc", nil)
|
|
return types.NewNamed(obj, types.Typ[types.String], nil)
|
|
}(),
|
|
},
|
|
{
|
|
name: "pointer to non-context type",
|
|
typ: func() types.Type {
|
|
pkg := types.NewPackage("net/http", "http")
|
|
obj := types.NewTypeName(token.NoPos, pkg, "Request", nil)
|
|
return types.NewPointer(types.NewNamed(obj, types.NewStruct(nil, nil), nil))
|
|
}(),
|
|
},
|
|
{
|
|
name: "nil type",
|
|
typ: nil,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if isContextType(tc.typ) {
|
|
t.Fatalf("expected isContextType to return false for %s", tc.name)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewIssueReturnsEmptyWhenPositionCannotBeResolved(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
iss := newIssue("T001", "desc", token.NewFileSet(), token.NoPos, issue.High, issue.High)
|
|
if iss.RuleID != "" || iss.File != "" {
|
|
t.Fatalf("expected empty issue for unresolved position, got %+v", iss)
|
|
}
|
|
}
|
|
|
|
// ── lookupNamedType ───────────────────────────────────────────────────────────
|
|
|
|
func TestLookupNamedTypeNoDot(t *testing.T) {
|
|
t.Parallel()
|
|
// A path with no dot must return nil before touching prog.
|
|
if got := lookupNamedType("nodot", nil); got != nil {
|
|
t.Fatalf("expected nil for path with no dot, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestLookupNamedTypePackageNotInProgram(t *testing.T) {
|
|
t.Parallel()
|
|
prog := ssa.NewProgram(token.NewFileSet(), 0)
|
|
// Program is empty — the requested package is not present.
|
|
if got := lookupNamedType("net/http.ResponseWriter", prog); got != nil {
|
|
t.Fatalf("expected nil when package is absent from program, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestLookupNamedTypeFound(t *testing.T) {
|
|
t.Parallel()
|
|
prog := ssa.NewProgram(token.NewFileSet(), 0)
|
|
|
|
// Manually construct a net/http package with ResponseWriter in its scope.
|
|
httpPkg := types.NewPackage("net/http", "http")
|
|
iface := types.NewInterfaceType(nil, nil)
|
|
obj := types.NewTypeName(token.NoPos, httpPkg, "ResponseWriter", nil)
|
|
_ = types.NewNamed(obj, iface, nil)
|
|
httpPkg.Scope().Insert(obj)
|
|
httpPkg.MarkComplete()
|
|
prog.CreatePackage(httpPkg, nil, nil, false)
|
|
|
|
got := lookupNamedType("net/http.ResponseWriter", prog)
|
|
if got == nil {
|
|
t.Fatal("expected non-nil type for known type in program")
|
|
}
|
|
named, ok := got.(*types.Named)
|
|
if !ok {
|
|
t.Fatalf("expected *types.Named, got %T", got)
|
|
}
|
|
if named.Obj().Name() != "ResponseWriter" {
|
|
t.Fatalf("expected name ResponseWriter, got %s", named.Obj().Name())
|
|
}
|
|
}
|
|
|
|
func TestLookupNamedTypeMemberIsNotTypeName(t *testing.T) {
|
|
t.Parallel()
|
|
prog := ssa.NewProgram(token.NewFileSet(), 0)
|
|
|
|
// Insert a Var (not a TypeName) into the package scope.
|
|
pkg := types.NewPackage("mylib", "mylib")
|
|
varObj := types.NewVar(token.NoPos, pkg, "SomeVar", types.Typ[types.String])
|
|
pkg.Scope().Insert(varObj)
|
|
pkg.MarkComplete()
|
|
prog.CreatePackage(pkg, nil, nil, false)
|
|
|
|
// SomeVar is a *types.Var, not a *types.TypeName — lookup must return nil.
|
|
if got := lookupNamedType("mylib.SomeVar", prog); got != nil {
|
|
t.Fatalf("expected nil for non-TypeName member, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestLookupNamedTypeMemberNotInScope(t *testing.T) {
|
|
t.Parallel()
|
|
prog := ssa.NewProgram(token.NewFileSet(), 0)
|
|
|
|
// Package with the right path but the requested name is absent from scope.
|
|
pkg := types.NewPackage("net/http", "http")
|
|
pkg.MarkComplete()
|
|
prog.CreatePackage(pkg, nil, nil, false)
|
|
|
|
// "Missing" is not in scope — exercises the member==nil continue branch.
|
|
if got := lookupNamedType("net/http.Missing", prog); got != nil {
|
|
t.Fatalf("expected nil for absent type name, got %v", got)
|
|
}
|
|
}
|
|
|
|
// ── guardsSatisfied ───────────────────────────────────────────────────────────
|
|
|
|
func TestGuardsSatisfiedEmptyGuards(t *testing.T) {
|
|
t.Parallel()
|
|
if !guardsSatisfied(nil, Sink{}, nil) {
|
|
t.Fatal("expected true for empty ArgTypeGuards")
|
|
}
|
|
}
|
|
|
|
func TestGuardsSatisfiedNilProg(t *testing.T) {
|
|
t.Parallel()
|
|
sink := Sink{ArgTypeGuards: map[int]string{0: "net/http.ResponseWriter"}}
|
|
if !guardsSatisfied(nil, sink, nil) {
|
|
t.Fatal("expected true when prog is nil")
|
|
}
|
|
}
|
|
|
|
func TestGuardsSatisfiedArgIdxOutOfRange(t *testing.T) {
|
|
t.Parallel()
|
|
prog := ssa.NewProgram(token.NewFileSet(), 0)
|
|
sink := Sink{ArgTypeGuards: map[int]string{0: "net/http.ResponseWriter"}}
|
|
// Guard requires arg at index 0 but args slice is empty.
|
|
if guardsSatisfied([]ssa.Value{}, sink, prog) {
|
|
t.Fatal("expected false when arg index is out of range")
|
|
}
|
|
}
|
|
|
|
func TestGuardsSatisfiedRequiredTypeNotFound(t *testing.T) {
|
|
t.Parallel()
|
|
prog := ssa.NewProgram(token.NewFileSet(), 0)
|
|
// Guard refers to a type that is not present in the program.
|
|
// The guard must not be satisfied.
|
|
sink := Sink{ArgTypeGuards: map[int]string{0: "missing/pkg.Type"}}
|
|
arg := ssa.NewConst(constant.MakeString("x"), types.Typ[types.String])
|
|
if guardsSatisfied([]ssa.Value{arg}, sink, prog) {
|
|
t.Fatal("expected false when required type is not found")
|
|
}
|
|
}
|
|
|
|
func TestGuardsSatisfiedInterfaceNotSatisfied(t *testing.T) {
|
|
t.Parallel()
|
|
prog := ssa.NewProgram(token.NewFileSet(), 0)
|
|
|
|
// Build an interface with one method; string doesn't implement it.
|
|
pkg := types.NewPackage("io", "io")
|
|
sig := types.NewSignatureType(nil, nil, nil, nil, nil, false)
|
|
closeMethod := types.NewFunc(token.NoPos, pkg, "Close", sig)
|
|
closerIface := types.NewInterfaceType([]*types.Func{closeMethod}, nil)
|
|
closerIface.Complete()
|
|
obj := types.NewTypeName(token.NoPos, pkg, "Closer", nil)
|
|
_ = types.NewNamed(obj, closerIface, nil)
|
|
pkg.Scope().Insert(obj)
|
|
pkg.MarkComplete()
|
|
prog.CreatePackage(pkg, nil, nil, false)
|
|
|
|
arg := ssa.NewConst(constant.MakeString("x"), types.Typ[types.String])
|
|
sink := Sink{ArgTypeGuards: map[int]string{0: "io.Closer"}}
|
|
if guardsSatisfied([]ssa.Value{arg}, sink, prog) {
|
|
t.Fatal("expected false when arg type does not implement required interface")
|
|
}
|
|
}
|
|
|
|
func TestGuardsSatisfiedEmptyInterfaceSatisfied(t *testing.T) {
|
|
t.Parallel()
|
|
prog := ssa.NewProgram(token.NewFileSet(), 0)
|
|
|
|
// Empty interface — every type satisfies it.
|
|
pkg := types.NewPackage("any/pkg", "pkg")
|
|
emptyIface := types.NewInterfaceType(nil, nil)
|
|
emptyIface.Complete()
|
|
obj := types.NewTypeName(token.NoPos, pkg, "AnyType", nil)
|
|
_ = types.NewNamed(obj, emptyIface, nil)
|
|
pkg.Scope().Insert(obj)
|
|
pkg.MarkComplete()
|
|
prog.CreatePackage(pkg, nil, nil, false)
|
|
|
|
arg := ssa.NewConst(constant.MakeString("x"), types.Typ[types.String])
|
|
sink := Sink{ArgTypeGuards: map[int]string{0: "any/pkg.AnyType"}}
|
|
if !guardsSatisfied([]ssa.Value{arg}, sink, prog) {
|
|
t.Fatal("expected true when arg implements empty interface")
|
|
}
|
|
}
|
|
|
|
func TestGuardsSatisfiedConcreteTypeNotSatisfied(t *testing.T) {
|
|
t.Parallel()
|
|
prog := ssa.NewProgram(token.NewFileSet(), 0)
|
|
|
|
// Named struct type — string is not identical to it.
|
|
pkg := types.NewPackage("myapp", "myapp")
|
|
obj := types.NewTypeName(token.NoPos, pkg, "MyStruct", nil)
|
|
_ = types.NewNamed(obj, types.NewStruct(nil, nil), nil)
|
|
pkg.Scope().Insert(obj)
|
|
pkg.MarkComplete()
|
|
prog.CreatePackage(pkg, nil, nil, false)
|
|
|
|
arg := ssa.NewConst(constant.MakeString("x"), types.Typ[types.String])
|
|
sink := Sink{ArgTypeGuards: map[int]string{0: "myapp.MyStruct"}}
|
|
// string != myapp.MyStruct and string != *myapp.MyStruct → guard not satisfied.
|
|
if guardsSatisfied([]ssa.Value{arg}, sink, prog) {
|
|
t.Fatal("expected false when arg type does not match required concrete type")
|
|
}
|
|
}
|
|
|
|
// ── resolveOriginalType ───────────────────────────────────────────────────────
|
|
|
|
func TestResolveOriginalTypeDefault(t *testing.T) {
|
|
t.Parallel()
|
|
// A plain Const value — no ChangeInterface or MakeInterface wrapping.
|
|
val := ssa.NewConst(constant.MakeString("test"), types.Typ[types.String])
|
|
got := resolveOriginalType(val)
|
|
if !types.Identical(got, types.Typ[types.String]) {
|
|
t.Fatalf("expected string type, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestAnalyzeSetsProgAndBuildsCallGraph(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Build a minimal self-contained package with a local interface W.
|
|
// Function f calls w.Write() which is configured as a sink below.
|
|
src := `package p
|
|
|
|
type W interface{ Write([]byte) (int, error) }
|
|
type B struct{}
|
|
|
|
func (b *B) Write(p []byte) (int, error) { return 0, nil }
|
|
func f(w W) { w.Write([]byte("hello")) }
|
|
`
|
|
fset := token.NewFileSet()
|
|
parsed, err := parser.ParseFile(fset, "p.go", src, 0)
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
info := &types.Info{
|
|
Types: make(map[ast.Expr]types.TypeAndValue),
|
|
Defs: make(map[*ast.Ident]types.Object),
|
|
Uses: make(map[*ast.Ident]types.Object),
|
|
Implicits: make(map[ast.Node]types.Object),
|
|
Scopes: make(map[ast.Node]*types.Scope),
|
|
Selections: make(map[*ast.SelectorExpr]*types.Selection),
|
|
}
|
|
pkg, err := (&types.Config{}).Check("p", fset, []*ast.File{parsed}, info)
|
|
if err != nil {
|
|
t.Fatalf("type-check: %v", err)
|
|
}
|
|
prog := ssa.NewProgram(fset, ssa.BuilderMode(0))
|
|
ssaPkg := prog.CreatePackage(pkg, []*ast.File{parsed}, info, true)
|
|
prog.Build()
|
|
|
|
fn := ssaPkg.Func("f")
|
|
if fn == nil {
|
|
t.Fatal("SSA function f not found")
|
|
}
|
|
|
|
// Sink matches the invoke call w.Write inside f; ArgTypeGuards left empty
|
|
// so guardsSatisfied is reached and returns true without further work.
|
|
analyzer := New(&Config{
|
|
Sinks: []Sink{
|
|
{Package: "p", Receiver: "W", Method: "Write"},
|
|
},
|
|
})
|
|
|
|
_ = analyzer.Analyze(prog, []*ssa.Function{fn})
|
|
}
|
|
|
|
// buildManySinkCallsFixture creates an SSA program with many interface implementations
|
|
// and many sink-calling functions, producing a large CHA call graph. Used by both the
|
|
// regression test and benchmark.
|
|
func buildManySinkCallsFixture(tb testing.TB) (*ssa.Program, []*ssa.Function) {
|
|
tb.Helper()
|
|
|
|
src := `package p
|
|
|
|
type W interface{ Write([]byte) (int, error) }
|
|
`
|
|
// Generate 20 concrete implementations of W to inflate CHA edges.
|
|
for i := 0; i < 20; i++ {
|
|
src += fmt.Sprintf(`
|
|
type Impl%d struct{}
|
|
func (x *Impl%d) Write(p []byte) (int, error) { return len(p), nil }
|
|
`, i, i)
|
|
}
|
|
|
|
// Generate 20 functions, each calling w.Write with a variable arg (potential sink).
|
|
for i := 0; i < 20; i++ {
|
|
src += fmt.Sprintf(`
|
|
func caller%d(w W, data []byte) { w.Write(data) }
|
|
`, i)
|
|
}
|
|
|
|
fset := token.NewFileSet()
|
|
parsed, err := parser.ParseFile(fset, "p.go", src, 0)
|
|
if err != nil {
|
|
tb.Fatalf("parse: %v", err)
|
|
}
|
|
info := &types.Info{
|
|
Types: make(map[ast.Expr]types.TypeAndValue),
|
|
Defs: make(map[*ast.Ident]types.Object),
|
|
Uses: make(map[*ast.Ident]types.Object),
|
|
Implicits: make(map[ast.Node]types.Object),
|
|
Scopes: make(map[ast.Node]*types.Scope),
|
|
Selections: make(map[*ast.SelectorExpr]*types.Selection),
|
|
}
|
|
pkg, err := (&types.Config{}).Check("p", fset, []*ast.File{parsed}, info)
|
|
if err != nil {
|
|
tb.Fatalf("type-check: %v", err)
|
|
}
|
|
prog := ssa.NewProgram(fset, ssa.BuilderMode(0))
|
|
ssaPkg := prog.CreatePackage(pkg, []*ast.File{parsed}, info, true)
|
|
prog.Build()
|
|
|
|
// Collect all caller* functions as analysis targets.
|
|
var srcFuncs []*ssa.Function
|
|
for i := 0; i < 20; i++ {
|
|
fn := ssaPkg.Func(fmt.Sprintf("caller%d", i))
|
|
if fn == nil {
|
|
tb.Fatalf("SSA function caller%d not found", i)
|
|
}
|
|
srcFuncs = append(srcFuncs, fn)
|
|
}
|
|
|
|
return prog, srcFuncs
|
|
}
|
|
|
|
func TestTaintAnalysisPerformanceWithManySinkCalls(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// This test verifies that taint analysis completes in bounded time even when
|
|
// CHA produces a large call graph (many interface implementations × many sink calls).
|
|
// Before the maxCallerEdges cap and paramTaintCache, this scenario could hang.
|
|
prog, srcFuncs := buildManySinkCallsFixture(t)
|
|
|
|
analyzer := New(&Config{
|
|
Sinks: []Sink{
|
|
{Package: "p", Receiver: "W", Method: "Write", CheckArgs: []int{1}},
|
|
},
|
|
})
|
|
|
|
// Must complete within 10 seconds; without the fix this could hang indefinitely.
|
|
done := make(chan []Result, 1)
|
|
go func() {
|
|
done <- analyzer.Analyze(prog, srcFuncs)
|
|
}()
|
|
|
|
select {
|
|
case <-time.After(10 * time.Second):
|
|
t.Fatal("taint analysis did not complete within 10 seconds — possible hang regression")
|
|
case results := <-done:
|
|
_ = results
|
|
}
|
|
}
|
|
|
|
func BenchmarkTaintAnalysisManySinkCalls(b *testing.B) {
|
|
prog, srcFuncs := buildManySinkCallsFixture(b)
|
|
|
|
cfg := &Config{
|
|
Sinks: []Sink{
|
|
{Package: "p", Receiver: "W", Method: "Write", CheckArgs: []int{1}},
|
|
},
|
|
}
|
|
|
|
b.ResetTimer()
|
|
for b.Loop() {
|
|
analyzer := New(cfg)
|
|
analyzer.Analyze(prog, srcFuncs)
|
|
}
|
|
}
|
|
|
|
func TestResolveOriginalTypeMakeInterface(t *testing.T) {
|
|
t.Parallel()
|
|
// Build a minimal, self-contained SSA program (no external imports) that
|
|
// boxes a concrete *B value into interface W. This exercises the
|
|
// *ssa.MakeInterface branch of resolveOriginalType.
|
|
src := `package p
|
|
|
|
type W interface{ Write([]byte) (int, error) }
|
|
type B struct{}
|
|
|
|
func (b *B) Write(p []byte) (int, error) { return 0, nil }
|
|
func f() W { return &B{} }
|
|
`
|
|
fset := token.NewFileSet()
|
|
parsed, err := parser.ParseFile(fset, "p.go", src, 0)
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
info := &types.Info{
|
|
Types: make(map[ast.Expr]types.TypeAndValue),
|
|
Defs: make(map[*ast.Ident]types.Object),
|
|
Uses: make(map[*ast.Ident]types.Object),
|
|
Implicits: make(map[ast.Node]types.Object),
|
|
Scopes: make(map[ast.Node]*types.Scope),
|
|
Selections: make(map[*ast.SelectorExpr]*types.Selection),
|
|
}
|
|
pkg, err := (&types.Config{}).Check("p", fset, []*ast.File{parsed}, info)
|
|
if err != nil {
|
|
t.Fatalf("type-check: %v", err)
|
|
}
|
|
prog := ssa.NewProgram(fset, ssa.BuilderMode(0))
|
|
ssaPkg := prog.CreatePackage(pkg, []*ast.File{parsed}, info, true)
|
|
prog.Build()
|
|
|
|
fn := ssaPkg.Func("f")
|
|
if fn == nil {
|
|
t.Fatal("SSA function f not found")
|
|
}
|
|
for _, blk := range fn.Blocks {
|
|
for _, instr := range blk.Instrs {
|
|
mi, ok := instr.(*ssa.MakeInterface)
|
|
if !ok {
|
|
continue
|
|
}
|
|
got := resolveOriginalType(mi)
|
|
if got == nil {
|
|
t.Fatal("resolveOriginalType returned nil for MakeInterface")
|
|
}
|
|
return
|
|
}
|
|
}
|
|
t.Fatal("no MakeInterface instruction found in function f")
|
|
}
|
|
|
|
// ── mayHaveExternalCallers ──────────────────────────────────────────────────
|
|
|
|
// makeHTTPPkg builds a synthetic net/http package with ResponseWriter and
|
|
// Request types, matching the real package path "net/http".
|
|
// This avoids depending on go/importer which may not resolve stdlib in CI.
|
|
func makeHTTPPkg() *types.Package {
|
|
httpPkg := types.NewPackage("net/http", "http")
|
|
|
|
// ResponseWriter — named interface.
|
|
rwIface := types.NewInterfaceType(nil, nil)
|
|
rwIface.Complete()
|
|
rwObj := types.NewTypeName(token.NoPos, httpPkg, "ResponseWriter", nil)
|
|
types.NewNamed(rwObj, rwIface, nil)
|
|
httpPkg.Scope().Insert(rwObj)
|
|
|
|
// Request — named struct.
|
|
reqObj := types.NewTypeName(token.NoPos, httpPkg, "Request", nil)
|
|
types.NewNamed(reqObj, types.NewStruct(nil, nil), nil)
|
|
httpPkg.Scope().Insert(reqObj)
|
|
|
|
httpPkg.MarkComplete()
|
|
return httpPkg
|
|
}
|
|
|
|
// makeFuncSSA creates an ssa.Function with the given signature and optional
|
|
// receiver, attached to a trivial SSA program. The function has no body.
|
|
func makeFuncSSA(t *testing.T, name string, sig *types.Signature) *ssa.Function {
|
|
t.Helper()
|
|
fset := token.NewFileSet()
|
|
prog := ssa.NewProgram(fset, 0)
|
|
pkg := types.NewPackage("p", "p")
|
|
pkg.MarkComplete()
|
|
ssaPkg := prog.CreatePackage(pkg, nil, nil, false)
|
|
|
|
fn := ssaPkg.Prog.NewFunction(name, sig, "test")
|
|
return fn
|
|
}
|
|
|
|
func TestMayHaveExternalCallers(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
simpleSig := types.NewSignatureType(nil, nil, nil,
|
|
types.NewTuple(types.NewVar(token.NoPos, nil, "x", types.Typ[types.Int])),
|
|
nil, false)
|
|
|
|
cases := []struct {
|
|
name string
|
|
fn func() *ssa.Function
|
|
want bool
|
|
}{
|
|
{
|
|
name: "ExportedBareFunc",
|
|
fn: func() *ssa.Function {
|
|
return makeFuncSSA(t, "Handler", simpleSig)
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "UnexportedBareFunc",
|
|
fn: func() *ssa.Function {
|
|
return makeFuncSSA(t, "handler", simpleSig)
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "MethodWithReceiver",
|
|
fn: func() *ssa.Function {
|
|
recv := types.NewVar(token.NoPos, nil, "s", types.NewPointer(types.NewStruct(nil, nil)))
|
|
methodSig := types.NewSignatureType(recv, nil, nil,
|
|
types.NewTuple(types.NewVar(token.NoPos, nil, "x", types.Typ[types.Int])),
|
|
nil, false)
|
|
return makeFuncSSA(t, "Do", methodSig)
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "NilSignature",
|
|
fn: func() *ssa.Function {
|
|
return &ssa.Function{}
|
|
},
|
|
want: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
fn := tc.fn()
|
|
got := mayHaveExternalCallers(fn)
|
|
if got != tc.want {
|
|
t.Errorf("mayHaveExternalCallers(%s) = %v, want %v", tc.name, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMayHaveExternalCallersClosureReturnsFalse(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// A closure (fn.Parent() != nil) is never exported, even if its
|
|
// synthesized name starts with an uppercase letter.
|
|
src := `package p
|
|
|
|
func Outer() {
|
|
fn := func(x int) { _ = x }
|
|
fn(1)
|
|
}
|
|
`
|
|
fset := token.NewFileSet()
|
|
parsed, err := parser.ParseFile(fset, "p.go", src, 0)
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
info := &types.Info{
|
|
Types: make(map[ast.Expr]types.TypeAndValue), Defs: make(map[*ast.Ident]types.Object),
|
|
Uses: make(map[*ast.Ident]types.Object), Implicits: make(map[ast.Node]types.Object),
|
|
Scopes: make(map[ast.Node]*types.Scope), Selections: make(map[*ast.SelectorExpr]*types.Selection),
|
|
}
|
|
pkg, _ := (&types.Config{}).Check("p", fset, []*ast.File{parsed}, info)
|
|
prog := ssa.NewProgram(fset, 0)
|
|
ssaPkg := prog.CreatePackage(pkg, []*ast.File{parsed}, info, true)
|
|
prog.Build()
|
|
|
|
outer := ssaPkg.Func("Outer")
|
|
if outer == nil {
|
|
t.Fatal("Outer not found")
|
|
}
|
|
// Find the anonymous closure inside Outer.
|
|
for _, anon := range outer.AnonFuncs {
|
|
if mayHaveExternalCallers(anon) {
|
|
t.Errorf("mayHaveExternalCallers(closure %s) = true, want false", anon.Name())
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── isParameterTainted entry-point logic ────────────────────────────────────
|
|
|
|
func TestIsParameterTaintedExportedFuncWithCallersStillTainted(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// An exported bare function with a source-type param must be auto-tainted
|
|
// even when it has internal callers with safe args — because external
|
|
// callers (framework dispatch) may be invisible to the call graph.
|
|
httpPkg := makeHTTPPkg()
|
|
|
|
src := `package p
|
|
|
|
import "net/http"
|
|
|
|
func Handler(w http.ResponseWriter, r *http.Request) {}
|
|
|
|
func caller() {
|
|
Handler(nil, nil)
|
|
}
|
|
`
|
|
fset := token.NewFileSet()
|
|
parsed, err := parser.ParseFile(fset, "p.go", src, 0)
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
|
|
fakeImporter := fakeImporterFunc(func(path string) (*types.Package, error) {
|
|
if path == "net/http" {
|
|
return httpPkg, nil
|
|
}
|
|
return nil, fmt.Errorf("unknown import %q", path)
|
|
})
|
|
|
|
info := &types.Info{
|
|
Types: make(map[ast.Expr]types.TypeAndValue),
|
|
Defs: make(map[*ast.Ident]types.Object),
|
|
Uses: make(map[*ast.Ident]types.Object),
|
|
Implicits: make(map[ast.Node]types.Object),
|
|
Scopes: make(map[ast.Node]*types.Scope),
|
|
Selections: make(map[*ast.SelectorExpr]*types.Selection),
|
|
}
|
|
pkg, err := (&types.Config{Importer: fakeImporter}).Check("p", fset, []*ast.File{parsed}, info)
|
|
if err != nil {
|
|
t.Fatalf("type-check: %v", err)
|
|
}
|
|
|
|
prog := ssa.NewProgram(fset, 0)
|
|
prog.CreatePackage(httpPkg, nil, nil, false) // register net/http in SSA
|
|
ssaPkg := prog.CreatePackage(pkg, []*ast.File{parsed}, info, true)
|
|
prog.Build()
|
|
|
|
handlerFn := ssaPkg.Func("Handler")
|
|
if handlerFn == nil {
|
|
t.Fatal("Handler not found")
|
|
}
|
|
if len(handlerFn.Params) < 2 {
|
|
t.Fatal("expected Handler to have 2 params")
|
|
}
|
|
|
|
analyzer := New(&Config{
|
|
Sources: []Source{{Package: "net/http", Name: "Request", Pointer: true}},
|
|
})
|
|
|
|
var srcFuncs []*ssa.Function
|
|
for _, m := range ssaPkg.Members {
|
|
if fn, ok := m.(*ssa.Function); ok {
|
|
srcFuncs = append(srcFuncs, fn)
|
|
}
|
|
}
|
|
_ = analyzer.Analyze(prog, srcFuncs)
|
|
|
|
// Handler has callers (caller() calls it).
|
|
node := analyzer.callGraph.Nodes[handlerFn]
|
|
if node == nil || len(node.In) == 0 {
|
|
t.Fatal("expected Handler to have callers in the call graph")
|
|
}
|
|
|
|
// Despite callers, isParameterTainted must return true because the
|
|
// function is an exported bare function (mayHaveExternalCallers).
|
|
visited := make(map[ssa.Value]bool)
|
|
tainted := analyzer.isParameterTainted(handlerFn.Params[1], handlerFn, visited, 0)
|
|
if !tainted {
|
|
t.Fatal("expected *http.Request param of HTTP handler to be auto-tainted even with internal callers")
|
|
}
|
|
}
|
|
|
|
func TestIsParameterTaintedNonHandlerWithCallersNotAutoTainted(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Non-handler function accepting *http.Request with a safe internal caller
|
|
// must NOT be auto-tainted.
|
|
httpPkg := makeHTTPPkg()
|
|
|
|
src := `package p
|
|
|
|
import "net/http"
|
|
|
|
func wrapper(r *http.Request) {}
|
|
|
|
func caller() {
|
|
wrapper(nil)
|
|
}
|
|
`
|
|
fset := token.NewFileSet()
|
|
parsed, err := parser.ParseFile(fset, "p.go", src, 0)
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
|
|
fakeImporter := fakeImporterFunc(func(path string) (*types.Package, error) {
|
|
if path == "net/http" {
|
|
return httpPkg, nil
|
|
}
|
|
return nil, fmt.Errorf("unknown import %q", path)
|
|
})
|
|
|
|
info := &types.Info{
|
|
Types: make(map[ast.Expr]types.TypeAndValue),
|
|
Defs: make(map[*ast.Ident]types.Object),
|
|
Uses: make(map[*ast.Ident]types.Object),
|
|
Implicits: make(map[ast.Node]types.Object),
|
|
Scopes: make(map[ast.Node]*types.Scope),
|
|
Selections: make(map[*ast.SelectorExpr]*types.Selection),
|
|
}
|
|
pkg, err := (&types.Config{Importer: fakeImporter}).Check("p", fset, []*ast.File{parsed}, info)
|
|
if err != nil {
|
|
t.Fatalf("type-check: %v", err)
|
|
}
|
|
|
|
prog := ssa.NewProgram(fset, 0)
|
|
prog.CreatePackage(httpPkg, nil, nil, false) // register net/http in SSA
|
|
ssaPkg := prog.CreatePackage(pkg, []*ast.File{parsed}, info, true)
|
|
prog.Build()
|
|
|
|
wrapperFn := ssaPkg.Func("wrapper")
|
|
if wrapperFn == nil {
|
|
t.Fatal("wrapper not found")
|
|
}
|
|
if len(wrapperFn.Params) < 1 {
|
|
t.Fatal("expected wrapper to have at least 1 param")
|
|
}
|
|
|
|
analyzer := New(&Config{
|
|
Sources: []Source{{Package: "net/http", Name: "Request", Pointer: true}},
|
|
})
|
|
|
|
var srcFuncs []*ssa.Function
|
|
for _, m := range ssaPkg.Members {
|
|
if fn, ok := m.(*ssa.Function); ok {
|
|
srcFuncs = append(srcFuncs, fn)
|
|
}
|
|
}
|
|
_ = analyzer.Analyze(prog, srcFuncs)
|
|
|
|
node := analyzer.callGraph.Nodes[wrapperFn]
|
|
if node == nil || len(node.In) == 0 {
|
|
t.Fatal("expected wrapper to have callers in the call graph")
|
|
}
|
|
|
|
visited := make(map[ssa.Value]bool)
|
|
tainted := analyzer.isParameterTainted(wrapperFn.Params[0], wrapperFn, visited, 0)
|
|
if tainted {
|
|
t.Fatal("expected *http.Request param of non-handler wrapper to NOT be auto-tainted when caller is safe")
|
|
}
|
|
}
|
|
|
|
// fakeImporterFunc adapts a function to the types.Importer interface.
|
|
type fakeImporterFunc func(path string) (*types.Package, error)
|
|
|
|
func (f fakeImporterFunc) Import(path string) (*types.Package, error) { return f(path) }
|
|
|
|
func TestIsParameterTaintedCacheHit(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// When isParameterTainted returns true for a handler param, the result is
|
|
// cached. A second call for the same param must hit the cache and return
|
|
// true immediately.
|
|
httpPkg := makeHTTPPkg()
|
|
|
|
src := `package p
|
|
|
|
import "net/http"
|
|
|
|
func handler(w http.ResponseWriter, r *http.Request) {}
|
|
`
|
|
fset := token.NewFileSet()
|
|
parsed, _ := parser.ParseFile(fset, "p.go", src, 0)
|
|
info := &types.Info{
|
|
Types: make(map[ast.Expr]types.TypeAndValue), Defs: make(map[*ast.Ident]types.Object),
|
|
Uses: make(map[*ast.Ident]types.Object), Implicits: make(map[ast.Node]types.Object),
|
|
Scopes: make(map[ast.Node]*types.Scope), Selections: make(map[*ast.SelectorExpr]*types.Selection),
|
|
}
|
|
pkg, _ := (&types.Config{Importer: fakeImporterFunc(func(path string) (*types.Package, error) {
|
|
if path == "net/http" {
|
|
return httpPkg, nil
|
|
}
|
|
return nil, fmt.Errorf("unknown %q", path)
|
|
})}).Check("p", fset, []*ast.File{parsed}, info)
|
|
|
|
prog := ssa.NewProgram(fset, 0)
|
|
prog.CreatePackage(httpPkg, nil, nil, false)
|
|
ssaPkg := prog.CreatePackage(pkg, []*ast.File{parsed}, info, true)
|
|
prog.Build()
|
|
|
|
handlerFn := ssaPkg.Func("handler")
|
|
reqParam := handlerFn.Params[1]
|
|
|
|
analyzer := New(&Config{
|
|
Sources: []Source{{Package: "net/http", Name: "Request", Pointer: true}},
|
|
})
|
|
var srcFuncs []*ssa.Function
|
|
for _, m := range ssaPkg.Members {
|
|
if fn, ok := m.(*ssa.Function); ok {
|
|
srcFuncs = append(srcFuncs, fn)
|
|
}
|
|
}
|
|
_ = analyzer.Analyze(prog, srcFuncs)
|
|
|
|
// First call populates cache.
|
|
visited1 := make(map[ssa.Value]bool)
|
|
if !analyzer.isParameterTainted(reqParam, handlerFn, visited1, 0) {
|
|
t.Fatal("first call: expected tainted")
|
|
}
|
|
|
|
// Second call must hit the cache (lines 895-898).
|
|
visited2 := make(map[ssa.Value]bool)
|
|
if !analyzer.isParameterTainted(reqParam, handlerFn, visited2, 0) {
|
|
t.Fatal("second call (cache hit): expected tainted")
|
|
}
|
|
}
|
|
|
|
func TestIsParameterTaintedNoCallGraph(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// When callGraph is nil, isParameterTainted falls back to type-based
|
|
// auto-taint for source-typed params and returns false otherwise.
|
|
httpPkg := makeHTTPPkg()
|
|
|
|
src := `package p
|
|
|
|
import "net/http"
|
|
|
|
func twoParams(r *http.Request, s string) {}
|
|
`
|
|
fset := token.NewFileSet()
|
|
parsed, _ := parser.ParseFile(fset, "p.go", src, 0)
|
|
info := &types.Info{
|
|
Types: make(map[ast.Expr]types.TypeAndValue), Defs: make(map[*ast.Ident]types.Object),
|
|
Uses: make(map[*ast.Ident]types.Object), Implicits: make(map[ast.Node]types.Object),
|
|
Scopes: make(map[ast.Node]*types.Scope), Selections: make(map[*ast.SelectorExpr]*types.Selection),
|
|
}
|
|
pkg, _ := (&types.Config{Importer: fakeImporterFunc(func(path string) (*types.Package, error) {
|
|
if path == "net/http" {
|
|
return httpPkg, nil
|
|
}
|
|
return nil, fmt.Errorf("unknown %q", path)
|
|
})}).Check("p", fset, []*ast.File{parsed}, info)
|
|
|
|
prog := ssa.NewProgram(fset, 0)
|
|
prog.CreatePackage(httpPkg, nil, nil, false)
|
|
ssaPkg := prog.CreatePackage(pkg, []*ast.File{parsed}, info, true)
|
|
prog.Build()
|
|
|
|
fn := ssaPkg.Func("twoParams")
|
|
if fn == nil || len(fn.Params) < 2 {
|
|
t.Fatal("expected twoParams with 2 params")
|
|
}
|
|
|
|
analyzer := New(&Config{
|
|
Sources: []Source{{Package: "net/http", Name: "Request", Pointer: true}},
|
|
})
|
|
// Do NOT call Analyze — callGraph stays nil.
|
|
// Initialize paramTaintCache so the cache-store branch is exercised.
|
|
analyzer.paramTaintCache = make(map[paramKey]bool)
|
|
|
|
// Source-type param → auto-taint (and caches result).
|
|
visited := make(map[ssa.Value]bool)
|
|
if !analyzer.isParameterTainted(fn.Params[0], fn, visited, 0) {
|
|
t.Fatal("expected source-type param to be auto-tainted when callGraph is nil")
|
|
}
|
|
|
|
// Verify cache was populated.
|
|
if !analyzer.paramTaintCache[paramKey{fn: fn, paramIdx: 0}] {
|
|
t.Fatal("expected cache to contain taint result for param 0")
|
|
}
|
|
|
|
// Non-source-type param → false.
|
|
visited2 := make(map[ssa.Value]bool)
|
|
if analyzer.isParameterTainted(fn.Params[1], fn, visited2, 0) {
|
|
t.Fatal("expected non-source-type param to NOT be tainted when callGraph is nil")
|
|
}
|
|
}
|
|
|
|
func TestIsParameterTaintedDepthExceeded(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// When recursion depth exceeds maxTaintDepth, isParameterTainted returns false.
|
|
httpPkg := makeHTTPPkg()
|
|
|
|
src := `package p
|
|
|
|
import "net/http"
|
|
|
|
func handler(w http.ResponseWriter, r *http.Request) {}
|
|
`
|
|
fset := token.NewFileSet()
|
|
parsed, _ := parser.ParseFile(fset, "p.go", src, 0)
|
|
info := &types.Info{
|
|
Types: make(map[ast.Expr]types.TypeAndValue), Defs: make(map[*ast.Ident]types.Object),
|
|
Uses: make(map[*ast.Ident]types.Object), Implicits: make(map[ast.Node]types.Object),
|
|
Scopes: make(map[ast.Node]*types.Scope), Selections: make(map[*ast.SelectorExpr]*types.Selection),
|
|
}
|
|
pkg, _ := (&types.Config{Importer: fakeImporterFunc(func(path string) (*types.Package, error) {
|
|
if path == "net/http" {
|
|
return httpPkg, nil
|
|
}
|
|
return nil, fmt.Errorf("unknown %q", path)
|
|
})}).Check("p", fset, []*ast.File{parsed}, info)
|
|
|
|
prog := ssa.NewProgram(fset, 0)
|
|
prog.CreatePackage(httpPkg, nil, nil, false)
|
|
ssaPkg := prog.CreatePackage(pkg, []*ast.File{parsed}, info, true)
|
|
prog.Build()
|
|
|
|
fn := ssaPkg.Func("handler")
|
|
if fn == nil || len(fn.Params) < 2 {
|
|
t.Fatal("expected handler with 2 params")
|
|
}
|
|
|
|
analyzer := New(&Config{
|
|
Sources: []Source{{Package: "net/http", Name: "Request", Pointer: true}},
|
|
})
|
|
|
|
visited := make(map[ssa.Value]bool)
|
|
// Passing depth > maxTaintDepth (50) → must return false.
|
|
if analyzer.isParameterTainted(fn.Params[1], fn, visited, maxTaintDepth+1) {
|
|
t.Fatal("expected false when depth exceeds maxTaintDepth")
|
|
}
|
|
}
|
|
|
|
func TestIsParameterTaintedEntryPointCacheStoreAndHit(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Exercises the cache-store (line 934) and cache-hit (line 897) branches.
|
|
// Analyze() sets paramTaintCache to nil on return, so we must invoke
|
|
// isParameterTainted directly while the cache is live. We do this by
|
|
// manually initialising the analyzer state the same way Analyze does.
|
|
httpPkg := makeHTTPPkg()
|
|
|
|
src := `package p
|
|
|
|
import "net/http"
|
|
|
|
func lonely(r *http.Request) {}
|
|
`
|
|
fset := token.NewFileSet()
|
|
parsed, _ := parser.ParseFile(fset, "p.go", src, 0)
|
|
info := &types.Info{
|
|
Types: make(map[ast.Expr]types.TypeAndValue), Defs: make(map[*ast.Ident]types.Object),
|
|
Uses: make(map[*ast.Ident]types.Object), Implicits: make(map[ast.Node]types.Object),
|
|
Scopes: make(map[ast.Node]*types.Scope), Selections: make(map[*ast.SelectorExpr]*types.Selection),
|
|
}
|
|
pkg, _ := (&types.Config{Importer: fakeImporterFunc(func(path string) (*types.Package, error) {
|
|
if path == "net/http" {
|
|
return httpPkg, nil
|
|
}
|
|
return nil, fmt.Errorf("unknown %q", path)
|
|
})}).Check("p", fset, []*ast.File{parsed}, info)
|
|
|
|
prog := ssa.NewProgram(fset, 0)
|
|
prog.CreatePackage(httpPkg, nil, nil, false)
|
|
ssaPkg := prog.CreatePackage(pkg, []*ast.File{parsed}, info, true)
|
|
prog.Build()
|
|
|
|
fn := ssaPkg.Func("lonely")
|
|
if fn == nil || len(fn.Params) < 1 {
|
|
t.Fatal("expected lonely with 1 param")
|
|
}
|
|
|
|
analyzer := New(&Config{
|
|
Sources: []Source{{Package: "net/http", Name: "Request", Pointer: true}},
|
|
})
|
|
// Manually set up call graph + cache (same as Analyze does internally).
|
|
analyzer.callGraph = cha.CallGraph(prog)
|
|
analyzer.paramTaintCache = make(map[paramKey]bool)
|
|
analyzer.prog = prog
|
|
|
|
// First call: entry point (no callers) + source type → auto-taint + cache store.
|
|
visited := make(map[ssa.Value]bool)
|
|
if !analyzer.isParameterTainted(fn.Params[0], fn, visited, 0) {
|
|
t.Fatal("expected entry-point source-type param to be tainted")
|
|
}
|
|
if !analyzer.paramTaintCache[paramKey{fn: fn, paramIdx: 0}] {
|
|
t.Fatal("expected cache to be populated")
|
|
}
|
|
|
|
// Second call: hits cache (line 897).
|
|
visited2 := make(map[ssa.Value]bool)
|
|
if !analyzer.isParameterTainted(fn.Params[0], fn, visited2, 0) {
|
|
t.Fatal("expected cache hit to return true")
|
|
}
|
|
}
|