Skip to content

Commit bd11fbe

Browse files
authored
fix: taint analysis false positives with G703,G705 (#1522)
* fix: taint analysis false positives with G703,G705 * additional tests * cross package coverage increase * Add additonal G118 tests for codecov * improve code coverage * field-level taint tracking and test coverage * add more tests * improve test coverage * fix unnecessary nosec comments for tool * improve code coverage * address codecov issues * improve code coverage
1 parent e34e8dd commit bd11fbe

File tree

12 files changed

+4055
-20
lines changed

12 files changed

+4055
-20
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ govulncheck: install-govulncheck
5757
fi
5858

5959
test-coverage:
60-
go test -race -v -count=1 -coverprofile=coverage.out ./...
60+
go test -race -v -count=1 -coverpkg=./... -coverprofile=coverage.out ./...
6161

6262
build:
6363
go build $(LDFLAGS) -o $(BIN) ./cmd/gosec/

analyzers/context_propagation.go

Lines changed: 139 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ func (s *contextPropagationState) detectLostCancel(fn *ssa.Function) {
234234
continue
235235
}
236236

237-
if !isCancelCalled(cancelValue) {
237+
if !isCancelCalled(cancelValue, s.ssaFuncs) {
238238
s.addIssue(instr.Pos(), msgLostCancel, issue.Medium, issue.High)
239239
}
240240
}
@@ -673,7 +673,7 @@ func isCancelFuncType(t types.Type) bool {
673673
return true
674674
}
675675

676-
func isCancelCalled(cancelValue ssa.Value) bool {
676+
func isCancelCalled(cancelValue ssa.Value, allFuncs []*ssa.Function) bool {
677677
if cancelValue == nil {
678678
return false
679679
}
@@ -699,6 +699,13 @@ func isCancelCalled(cancelValue ssa.Value) bool {
699699
if r.Val != current {
700700
continue
701701
}
702+
// Check if storing to a struct field — if so, search other
703+
// methods of the same type for loads of that field + call.
704+
if fa, ok := r.Addr.(*ssa.FieldAddr); ok {
705+
if isCancelCalledViaStructField(fa, allFuncs) {
706+
return true
707+
}
708+
}
702709
queue = append(queue, r.Addr)
703710
case *ssa.UnOp:
704711
if r.Op == token.MUL && r.X == current {
@@ -725,6 +732,136 @@ func isCancelCalled(cancelValue ssa.Value) bool {
725732
return false
726733
}
727734

735+
// isCancelCalledViaStructField checks whether a cancel function stored into a
736+
// struct field (e.g., job.cancelFn = cancel) is subsequently called in any other
737+
// method of the same receiver type (e.g., job.Close() calls job.cancelFn()).
738+
func isCancelCalledViaStructField(storeFA *ssa.FieldAddr, allFuncs []*ssa.Function) bool {
739+
// Get the field index and the receiver pointer type
740+
fieldIdx := storeFA.Field
741+
structPtrType := storeFA.X.Type()
742+
743+
for _, fn := range allFuncs {
744+
if fn == nil || fn.Blocks == nil {
745+
continue
746+
}
747+
// Only check methods on the same receiver type
748+
if fn.Signature == nil || fn.Signature.Recv() == nil {
749+
continue
750+
}
751+
if !types.Identical(fn.Signature.Recv().Type(), structPtrType) {
752+
continue
753+
}
754+
755+
// Look for a load of the same field followed by a call
756+
for _, block := range fn.Blocks {
757+
for _, instr := range block.Instrs {
758+
fa, ok := instr.(*ssa.FieldAddr)
759+
if !ok || fa.Field != fieldIdx {
760+
continue
761+
}
762+
// Check that this FieldAddr is on the receiver (Params[0])
763+
if len(fn.Params) == 0 {
764+
continue
765+
}
766+
if !reachesParam(fa.X, fn.Params[0]) {
767+
continue
768+
}
769+
// Check if the value loaded from this field is eventually called
770+
if isFieldValueCalled(fa) {
771+
return true
772+
}
773+
}
774+
}
775+
}
776+
return false
777+
}
778+
779+
// reachesParam checks if a value traces back to the given parameter,
780+
// following through pointer dereferences and phi nodes.
781+
func reachesParam(v ssa.Value, param *ssa.Parameter) bool {
782+
seen := make(map[ssa.Value]bool)
783+
return reachesParamImpl(v, param, seen)
784+
}
785+
786+
func reachesParamImpl(v ssa.Value, param *ssa.Parameter, seen map[ssa.Value]bool) bool {
787+
if v == nil || seen[v] {
788+
return false
789+
}
790+
seen[v] = true
791+
792+
if v == param {
793+
return true
794+
}
795+
switch val := v.(type) {
796+
case *ssa.UnOp:
797+
return reachesParamImpl(val.X, param, seen)
798+
case *ssa.Phi:
799+
for _, e := range val.Edges {
800+
if reachesParamImpl(e, param, seen) {
801+
return true
802+
}
803+
}
804+
case *ssa.FieldAddr:
805+
return reachesParamImpl(val.X, param, seen)
806+
}
807+
return false
808+
}
809+
810+
// isFieldValueCalled checks if the value loaded from a FieldAddr is eventually
811+
// used as a callee (i.e., the loaded function pointer is called).
812+
func isFieldValueCalled(fa *ssa.FieldAddr) bool {
813+
refs := fa.Referrers()
814+
if refs == nil {
815+
return false
816+
}
817+
for _, ref := range *refs {
818+
// Look for a load (UnOp MUL = pointer dereference)
819+
unop, ok := ref.(*ssa.UnOp)
820+
if !ok || unop.Op != token.MUL {
821+
continue
822+
}
823+
// Check if the loaded value is called
824+
loadRefs := unop.Referrers()
825+
if loadRefs == nil {
826+
continue
827+
}
828+
queue := []ssa.Value{unop}
829+
visited := make(map[ssa.Value]bool)
830+
for len(queue) > 0 {
831+
cur := queue[0]
832+
queue = queue[1:]
833+
if cur == nil || visited[cur] {
834+
continue
835+
}
836+
visited[cur] = true
837+
curRefs := cur.Referrers()
838+
if curRefs == nil {
839+
continue
840+
}
841+
for _, r := range *curRefs {
842+
switch rr := r.(type) {
843+
case ssa.CallInstruction:
844+
if isUsedInCall(rr.Common(), cur) {
845+
return true
846+
}
847+
case *ssa.Phi:
848+
queue = append(queue, rr)
849+
case *ssa.Store:
850+
// stored then loaded elsewhere — follow addr
851+
if rr.Val == cur {
852+
queue = append(queue, rr.Addr)
853+
}
854+
case *ssa.UnOp:
855+
if rr.X == cur {
856+
queue = append(queue, rr)
857+
}
858+
}
859+
}
860+
}
861+
}
862+
return false
863+
}
864+
728865
func isUsedInCall(common *ssa.CallCommon, target ssa.Value) bool {
729866
if common == nil || target == nil {
730867
return false

analyzers/loginjection.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,21 @@ func LogInjection() taint.Config {
5858
{Package: "strconv", Method: "Quote"},
5959
// url.QueryEscape encodes special characters
6060
{Package: "net/url", Method: "QueryEscape"},
61+
62+
// JSON encoding escapes all special characters including newlines,
63+
// producing structurally safe output for log entries.
64+
{Package: "encoding/json", Method: "Marshal"},
65+
{Package: "encoding/json", Method: "MarshalIndent"},
66+
67+
// Numeric conversions produce strings that cannot contain
68+
// log injection characters (newlines, carriage returns).
69+
{Package: "strconv", Method: "Atoi"},
70+
{Package: "strconv", Method: "Itoa"},
71+
{Package: "strconv", Method: "ParseInt"},
72+
{Package: "strconv", Method: "ParseUint"},
73+
{Package: "strconv", Method: "ParseFloat"},
74+
{Package: "strconv", Method: "FormatInt"},
75+
{Package: "strconv", Method: "FormatFloat"},
6176
},
6277
}
6378
}

analyzers/pathtraversal.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,20 @@ func PathTraversal() taint.Config {
7171
{Package: "path/filepath", Method: "Rel"},
7272
// url.PathEscape escapes path components
7373
{Package: "net/url", Method: "PathEscape"},
74+
75+
// path.Base and path.Clean provide identical traversal-stripping
76+
// semantics as their filepath counterparts (the only difference is
77+
// separator handling, which is irrelevant for security).
78+
{Package: "path", Method: "Base"},
79+
{Package: "path", Method: "Clean"},
80+
81+
// Integer conversions eliminate path traversal vectors entirely —
82+
// the result can never contain "/" or ".." characters.
83+
{Package: "strconv", Method: "Atoi"},
84+
{Package: "strconv", Method: "ParseInt"},
85+
{Package: "strconv", Method: "ParseUint"},
86+
{Package: "strconv", Method: "ParseFloat"},
87+
{Package: "strconv", Method: "ParseBool"},
7488
},
7589
}
7690
}

analyzers/xss.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,23 @@ func XSS() taint.Config {
5858
// url.QueryEscape for URL parameter escaping
5959
{Package: "net/url", Method: "QueryEscape"},
6060
{Package: "net/url", Method: "PathEscape"},
61+
62+
// JSON encoding produces structurally safe output that cannot
63+
// contain unescaped HTML tags or script injections. The output
64+
// is served as application/json, not text/html.
65+
{Package: "encoding/json", Method: "Marshal"},
66+
{Package: "encoding/json", Method: "MarshalIndent"},
67+
68+
// Integer/float conversions produce numeric strings that cannot
69+
// contain XSS payloads.
70+
{Package: "strconv", Method: "Atoi"},
71+
{Package: "strconv", Method: "Itoa"},
72+
{Package: "strconv", Method: "ParseInt"},
73+
{Package: "strconv", Method: "ParseUint"},
74+
{Package: "strconv", Method: "ParseFloat"},
75+
{Package: "strconv", Method: "FormatInt"},
76+
{Package: "strconv", Method: "FormatUint"},
77+
{Package: "strconv", Method: "FormatFloat"},
6178
},
6279
}
6380
}

cmd/gosecutil/tools.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,11 @@ func (u *utilities) run(args ...string) {
7676
func shouldSkip(path string) bool {
7777
st, e := os.Stat(path) // #nosec G703 -- gosecutil intentionally inspects user-supplied local file paths
7878
if e != nil {
79-
//#nosec
80-
fmt.Fprintf(os.Stderr, "Skipping: %s - %s\n", path, e)
79+
fmt.Fprintf(os.Stderr, "Skipping: %s - %s\n", path, e) //#nosec G705
8180
return true
8281
}
8382
if st.IsDir() {
84-
//#nosec
85-
fmt.Fprintf(os.Stderr, "Skipping: %s - directory\n", path)
83+
fmt.Fprintf(os.Stderr, "Skipping: %s - directory\n", path) //#nosec G705
8684
return true
8785
}
8886
return false
@@ -135,7 +133,9 @@ func createContext(filename string) *context {
135133
Scopes: make(map[ast.Node]*types.Scope),
136134
Implicits: make(map[ast.Node]types.Object),
137135
}
138-
config := types.Config{Importer: importer.Default()}
136+
// Use ForCompiler with "source" for more reliable import resolution
137+
// This reads from source files instead of relying on compiled packages
138+
config := types.Config{Importer: importer.ForCompiler(fileset, "source", nil)}
139139
pkg, e := config.Check("main.go", fileset, []*ast.File{root}, info)
140140
if e != nil {
141141
//#nosec

0 commit comments

Comments
 (0)