@@ -23,6 +23,12 @@ import (
2323// maxTaintDepth limits recursion depth to prevent stack overflow on large codebases
2424const maxTaintDepth = 50
2525
26+ // maxCallerEdges caps the number of incoming call graph edges examined per function
27+ // in isParameterTainted. CHA over-approximates call graphs (every interface method
28+ // call fans out to ALL implementations), so a function can have thousands of callers.
29+ // Real taint flows come from direct/nearby callers, not the 33rd+ CHA-generated edge.
30+ const maxCallerEdges = 32
31+
2632// isContextType checks if a type is context.Context.
2733// context.Context is a control-flow mechanism (deadlines, cancellation, request-scoped values)
2834// that does not carry user-controlled data relevant to taint sinks like XSS.
@@ -211,14 +217,21 @@ type Config struct {
211217}
212218
213219// Analyzer performs taint analysis on SSA programs.
220+ // paramKey identifies a specific parameter of a function for memoization.
221+ type paramKey struct {
222+ fn * ssa.Function
223+ paramIdx int
224+ }
225+
214226type Analyzer struct {
215- config * Config
216- sources map [string ]Source // keyed by full type string
217- funcSrcs map [string ]Source // function sources keyed by "pkg.Func"
218- sinks map [string ]Sink // keyed by full function string
219- sanitizers map [string ]struct {} // keyed by full function string
220- callGraph * callgraph.Graph
221- prog * ssa.Program // set at Analyze time for ArgTypeGuards resolution
227+ config * Config
228+ sources map [string ]Source // keyed by full type string
229+ funcSrcs map [string ]Source // function sources keyed by "pkg.Func"
230+ sinks map [string ]Sink // keyed by full function string
231+ sanitizers map [string ]struct {} // keyed by full function string
232+ callGraph * callgraph.Graph
233+ prog * ssa.Program // set at Analyze time for ArgTypeGuards resolution
234+ paramTaintCache map [paramKey ]bool // caches true results from isParameterTainted
222235}
223236
224237// SetCallGraph injects a precomputed call graph.
@@ -309,13 +322,17 @@ func (a *Analyzer) Analyze(prog *ssa.Program, srcFuncs []*ssa.Function) []Result
309322 a .callGraph = cha .CallGraph (prog )
310323 }
311324
325+ a .paramTaintCache = make (map [paramKey ]bool )
326+
312327 var results []Result
313328
314329 // Find all sink calls in the program
315330 for _ , fn := range srcFuncs {
316331 results = append (results , a .analyzeFunctionSinks (fn )... )
317332 }
318333
334+ a .paramTaintCache = nil
335+
319336 return results
320337}
321338
@@ -824,11 +841,31 @@ func (a *Analyzer) isParameterTainted(param *ssa.Parameter, fn *ssa.Function, vi
824841 return false
825842 }
826843
844+ // Resolve paramIdx early so we can use it for cache lookups.
845+ paramIdx := - 1
846+ for i , p := range fn .Params {
847+ if p == param {
848+ paramIdx = i
849+ break
850+ }
851+ }
852+
853+ // Check memoization cache (only true results are cached).
854+ if paramIdx >= 0 && a .paramTaintCache != nil {
855+ key := paramKey {fn : fn , paramIdx : paramIdx }
856+ if a .paramTaintCache [key ] {
857+ return true
858+ }
859+ }
860+
827861 // Check if parameter type is a source type.
828862 // This is the ONLY place where type-based source matching should trigger
829863 // automatic taint — because parameters represent data flowing IN from
830864 // external callers we don't control.
831865 if a .isSourceType (param .Type ()) {
866+ if paramIdx >= 0 && a .paramTaintCache != nil {
867+ a .paramTaintCache [paramKey {fn : fn , paramIdx : paramIdx }] = true
868+ }
832869 return true
833870 }
834871
@@ -842,14 +879,6 @@ func (a *Analyzer) isParameterTainted(param *ssa.Parameter, fn *ssa.Function, vi
842879 return false
843880 }
844881
845- paramIdx := - 1
846- for i , p := range fn .Params {
847- if p == param {
848- paramIdx = i
849- break
850- }
851- }
852-
853882 if paramIdx < 0 {
854883 return false
855884 }
@@ -867,8 +896,14 @@ func (a *Analyzer) isParameterTainted(param *ssa.Parameter, fn *ssa.Function, vi
867896 adjustedIdx = paramIdx
868897 }
869898
870- // Check each caller
899+ // Check each caller, capping at maxCallerEdges to avoid combinatorial
900+ // explosion from CHA over-approximation of interface method calls.
901+ edgesChecked := 0
871902 for _ , inEdge := range node .In {
903+ if edgesChecked >= maxCallerEdges {
904+ break
905+ }
906+
872907 site := inEdge .Site
873908 if site == nil {
874909 continue
@@ -877,7 +912,11 @@ func (a *Analyzer) isParameterTainted(param *ssa.Parameter, fn *ssa.Function, vi
877912 callArgs := site .Common ().Args
878913
879914 if adjustedIdx < len (callArgs ) {
915+ edgesChecked ++
880916 if a .isTainted (callArgs [adjustedIdx ], inEdge .Caller .Func , visited , depth + 1 ) {
917+ if a .paramTaintCache != nil {
918+ a .paramTaintCache [paramKey {fn : fn , paramIdx : paramIdx }] = true
919+ }
881920 return true
882921 }
883922 }
0 commit comments