Skip to content

Commit 619ce21

Browse files
authored
Fix infinite recursion in interprocedural taint analysis (#1594)
The interprocedural taint analysis functions isCalleValueTainted, isFieldOfAllocTaintedInCallee, and isFieldTaintedViaCall did not check the visited map before recursing. Since callee-scope SSA values are different objects from caller-scope ones, the visited map populated by isTainted never cached them, causing exponential recursion on codebases with multi-level constructor chains that fan out tainted config through struct fields. Add visited map checks at the entry of each interprocedural function to prevent re-analyzing the same SSA values, call sites, and allocations. Also add test cases exercising multi-level constructor chains, fan-out constructors, and deep nested struct field access to verify termination. Fixes #1587
1 parent 0e0eb17 commit 619ce21

File tree

2 files changed

+215
-0
lines changed

2 files changed

+215
-0
lines changed

taint/taint.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1073,6 +1073,12 @@ func (a *Analyzer) isFieldTaintedViaCall(call *ssa.Call, fieldIdx int, callee *s
10731073
return false
10741074
}
10751075

1076+
// Prevent re-analyzing the same call site
1077+
if visited[call] {
1078+
return false
1079+
}
1080+
visited[call] = true
1081+
10761082
// If we don't have SSA blocks (external function or no body), use fallback logic:
10771083
// Assume the field is tainted if any argument to the constructor is tainted.
10781084
if callee.Blocks == nil {
@@ -1114,6 +1120,11 @@ func (a *Analyzer) isFieldOfAllocTaintedInCallee(alloc *ssa.Alloc, fieldIdx int,
11141120
if alloc.Referrers() == nil || depth > maxTaintDepth {
11151121
return false
11161122
}
1123+
1124+
if visited[alloc] {
1125+
return false
1126+
}
1127+
visited[alloc] = true
11171128
for _, ref := range *alloc.Referrers() {
11181129
fa, ok := ref.(*ssa.FieldAddr)
11191130
if !ok || fa.Field != fieldIdx {
@@ -1144,6 +1155,12 @@ func (a *Analyzer) isCalleValueTainted(v ssa.Value, callee *ssa.Function, call *
11441155
return false
11451156
}
11461157

1158+
// Prevent infinite recursion on cyclic SSA value graphs
1159+
if visited[v] {
1160+
return false
1161+
}
1162+
visited[v] = true
1163+
11471164
// If the value is a callee parameter, map it to the caller's argument
11481165
if param, ok := v.(*ssa.Parameter); ok {
11491166
for i, p := range callee.Params {

testutils/g701_samples.go

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1998,4 +1998,202 @@ func handler(db *sql.DB, r *http.Request) {
19981998
db.Query(query)
19991999
}
20002000
`}, 1, gosec.NewConfig()},
2001+
2002+
// Multi-level constructor chain with shared tainted config (issue #1587)
2003+
// Tests that interprocedural taint analysis terminates when constructors
2004+
// fan out tainted config through multiple struct levels.
2005+
// The analysis terminates correctly but does not yet follow double field
2006+
// indirection (app.cfg.DSN), so no issue is expected.
2007+
{[]string{`
2008+
package main
2009+
2010+
import (
2011+
"database/sql"
2012+
"net/http"
2013+
)
2014+
2015+
type Config struct {
2016+
DSN string
2017+
}
2018+
2019+
type RepoLayer struct {
2020+
cfg Config
2021+
}
2022+
2023+
func NewRepoLayer(cfg Config) *RepoLayer {
2024+
return &RepoLayer{cfg: cfg}
2025+
}
2026+
2027+
type ServiceLayer struct {
2028+
repo *RepoLayer
2029+
cfg Config
2030+
}
2031+
2032+
func NewServiceLayer(cfg Config) *ServiceLayer {
2033+
return &ServiceLayer{
2034+
repo: NewRepoLayer(cfg),
2035+
cfg: cfg,
2036+
}
2037+
}
2038+
2039+
type App struct {
2040+
svc *ServiceLayer
2041+
cfg Config
2042+
}
2043+
2044+
func NewApp(cfg Config) *App {
2045+
return &App{
2046+
svc: NewServiceLayer(cfg),
2047+
cfg: cfg,
2048+
}
2049+
}
2050+
2051+
func handler(db *sql.DB, r *http.Request) {
2052+
cfg := Config{DSN: r.FormValue("dsn")}
2053+
app := NewApp(cfg)
2054+
query := "SELECT * FROM t WHERE dsn = '" + app.cfg.DSN + "'"
2055+
db.Query(query)
2056+
}
2057+
`}, 0, gosec.NewConfig()},
2058+
2059+
// Fan-out constructor with multiple children storing tainted data (issue #1587)
2060+
// Tests termination when one constructor creates multiple child structs that
2061+
// each store the same tainted parameter.
2062+
{[]string{`
2063+
package main
2064+
2065+
import (
2066+
"database/sql"
2067+
"net/http"
2068+
)
2069+
2070+
type ChildA struct {
2071+
val string
2072+
}
2073+
2074+
func NewChildA(v string) *ChildA {
2075+
return &ChildA{val: v}
2076+
}
2077+
2078+
type ChildB struct {
2079+
val string
2080+
}
2081+
2082+
func NewChildB(v string) *ChildB {
2083+
return &ChildB{val: v}
2084+
}
2085+
2086+
type ChildC struct {
2087+
val string
2088+
}
2089+
2090+
func NewChildC(v string) *ChildC {
2091+
return &ChildC{val: v}
2092+
}
2093+
2094+
type Parent struct {
2095+
a *ChildA
2096+
b *ChildB
2097+
c *ChildC
2098+
}
2099+
2100+
func NewParent(input string) *Parent {
2101+
return &Parent{
2102+
a: NewChildA(input),
2103+
b: NewChildB(input),
2104+
c: NewChildC(input),
2105+
}
2106+
}
2107+
2108+
func handler(db *sql.DB, r *http.Request) {
2109+
p := NewParent(r.FormValue("name"))
2110+
query := "SELECT * FROM t WHERE a = '" + p.a.val + "'"
2111+
db.Query(query)
2112+
}
2113+
`}, 1, gosec.NewConfig()},
2114+
2115+
// Deep nested struct field access through constructor chain (issue #1587)
2116+
// Tests that taint tracks correctly through deeply nested field access
2117+
// without exponential blowup from revisiting the same call sites.
2118+
{[]string{`
2119+
package main
2120+
2121+
import (
2122+
"database/sql"
2123+
"net/http"
2124+
)
2125+
2126+
type Inner struct {
2127+
data string
2128+
}
2129+
2130+
func NewInner(d string) *Inner {
2131+
return &Inner{data: d}
2132+
}
2133+
2134+
type Middle struct {
2135+
inner *Inner
2136+
}
2137+
2138+
func NewMiddle(d string) *Middle {
2139+
return &Middle{inner: NewInner(d)}
2140+
}
2141+
2142+
type Outer struct {
2143+
middle *Middle
2144+
}
2145+
2146+
func NewOuter(d string) *Outer {
2147+
return &Outer{middle: NewMiddle(d)}
2148+
}
2149+
2150+
func handler(db *sql.DB, r *http.Request) {
2151+
o := NewOuter(r.FormValue("q"))
2152+
query := "SELECT * FROM t WHERE v = '" + o.middle.inner.data + "'"
2153+
db.Query(query)
2154+
}
2155+
`}, 1, gosec.NewConfig()},
2156+
2157+
// No taint: safe value through multi-level constructors (issue #1587)
2158+
// Ensures no false positive when a constant flows through the same
2159+
// multi-level constructor chain.
2160+
{[]string{`
2161+
package main
2162+
2163+
import (
2164+
"database/sql"
2165+
"net/http"
2166+
)
2167+
2168+
type Cfg struct {
2169+
Host string
2170+
}
2171+
2172+
type Repo struct {
2173+
cfg Cfg
2174+
}
2175+
2176+
func NewRepo(cfg Cfg) *Repo {
2177+
return &Repo{cfg: cfg}
2178+
}
2179+
2180+
type Svc struct {
2181+
repo *Repo
2182+
cfg Cfg
2183+
}
2184+
2185+
func NewSvc(cfg Cfg) *Svc {
2186+
return &Svc{
2187+
repo: NewRepo(cfg),
2188+
cfg: cfg,
2189+
}
2190+
}
2191+
2192+
func handler(db *sql.DB, _ *http.Request) {
2193+
cfg := Cfg{Host: "localhost"}
2194+
svc := NewSvc(cfg)
2195+
query := "SELECT * FROM t WHERE host = '" + svc.cfg.Host + "'"
2196+
db.Query(query)
2197+
}
2198+
`}, 0, gosec.NewConfig()},
20012199
}

0 commit comments

Comments
 (0)