Files
gosec/taint/analyzer_internal_test.go
Ravi Sastry Kadali baf0dcfbec fix(taint): gate *http.Request auto-taint on entry-point detection (#1630)
* 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
2026-04-08 11:31:24 +02:00

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")
}
}