@@ -21,10 +21,11 @@ type secretSerialization struct {
2121}
2222
2323type formatSpec struct {
24- name string
25- tagKey string
26- functionSinks []functionSink
27- methodSinks []methodSink
24+ name string
25+ tagKey string
26+ marshalerMethod string // e.g. "MarshalJSON"; empty if no standard interface exists
27+ functionSinks []functionSink
28+ methodSinks []methodSink
2829}
2930
3031type functionSink struct {
@@ -44,15 +45,16 @@ type typeAnalysisCacheKey struct {
4445}
4546
4647type sensitiveFieldMatch struct {
47- fieldName string
48- jsonKey string
49- found bool
48+ fieldName string
49+ serializedKey string
50+ found bool
5051}
5152
5253var g117Formats = []formatSpec {
5354 {
54- name : "json" ,
55- tagKey : "json" ,
55+ name : "JSON" ,
56+ tagKey : "json" ,
57+ marshalerMethod : "MarshalJSON" ,
5658 functionSinks : []functionSink {
5759 {pkgPath : "encoding/json" , names : []string {"Marshal" , "MarshalIndent" }},
5860 },
@@ -61,8 +63,9 @@ var g117Formats = []formatSpec{
6163 },
6264 },
6365 {
64- name : "yaml" ,
65- tagKey : "yaml" ,
66+ name : "YAML" ,
67+ tagKey : "yaml" ,
68+ marshalerMethod : "MarshalYAML" ,
6669 functionSinks : []functionSink {
6770 {pkgPath : "go.yaml.in/yaml/v3" , names : []string {"Marshal" }},
6871 {pkgPath : "gopkg.in/yaml.v3" , names : []string {"Marshal" }},
@@ -76,8 +79,9 @@ var g117Formats = []formatSpec{
7679 },
7780 },
7881 {
79- name : "xml" ,
80- tagKey : "xml" ,
82+ name : "XML" ,
83+ tagKey : "xml" ,
84+ marshalerMethod : "MarshalXML" ,
8185 functionSinks : []functionSink {
8286 {pkgPath : "encoding/xml" , names : []string {"Marshal" , "MarshalIndent" }},
8387 },
@@ -86,7 +90,7 @@ var g117Formats = []formatSpec{
8690 },
8791 },
8892 {
89- name : "toml " ,
93+ name : "TOML " ,
9094 tagKey : "toml" ,
9195 functionSinks : []functionSink {
9296 {pkgPath : "github.com/pelletier/go-toml" , names : []string {"Marshal" }},
@@ -111,17 +115,146 @@ func (r *secretSerialization) Match(n ast.Node, ctx *gosec.Context) (*issue.Issu
111115 return nil , nil
112116 }
113117
118+ if isInsideCustomMarshaler (callExpr , ctx ) {
119+ return nil , nil
120+ }
121+
114122 typ := ctx .Info .TypeOf (serializedArg )
115123 if typ == nil {
116124 return nil , nil
117125 }
118126
119- if match := r .findSensitiveFieldForType (typ , format .tagKey ); match .found {
120- msg := fmt .Sprintf ("Marshaled struct field %q (JSON key %q) matches secret pattern" , match .fieldName , match .jsonKey )
121- return ctx .NewIssue (callExpr , r .ID (), msg , r .Severity , r .Confidence ), nil
127+ if typeImplementsMarshaler (typ , format .marshalerMethod ) {
128+ return nil , nil
129+ }
130+
131+ match := r .findSensitiveFieldForType (typ , format .tagKey )
132+ if ! match .found {
133+ return nil , nil
134+ }
135+
136+ if compositeLitFieldIsTransformed (serializedArg , match .fieldName ) {
137+ return nil , nil
138+ }
139+
140+ msg := fmt .Sprintf ("Marshaled struct field %q (%s key %q) matches secret pattern" , match .fieldName , format .name , match .serializedKey )
141+ return ctx .NewIssue (callExpr , r .ID (), msg , r .Severity , r .Confidence ), nil
142+ }
143+
144+ // customMarshalerMethods lists method names that indicate a custom marshaler
145+ // implementation. When a marshal call occurs inside one of these methods, the
146+ // developer is explicitly controlling serialization, so G117 should not flag it.
147+ var customMarshalerMethods = map [string ]bool {
148+ "MarshalJSON" : true ,
149+ "MarshalYAML" : true ,
150+ "MarshalXML" : true ,
151+ "MarshalText" : true ,
152+ "MarshalTOML" : true ,
153+ "MarshalBSON" : true ,
154+ }
155+
156+ // isInsideCustomMarshaler reports whether callExpr is located inside a method
157+ // whose name matches a known custom marshaler (e.g. MarshalJSON).
158+ func isInsideCustomMarshaler (callExpr * ast.CallExpr , ctx * gosec.Context ) bool {
159+ if ctx .Root == nil {
160+ return false
161+ }
162+
163+ pos := callExpr .Pos ()
164+ var found bool
165+
166+ ast .Inspect (ctx .Root , func (n ast.Node ) bool {
167+ if found {
168+ return false
169+ }
170+ funcDecl , ok := n .(* ast.FuncDecl )
171+ if ! ok || funcDecl .Body == nil {
172+ return true
173+ }
174+ // Check if the call is inside this function body.
175+ if pos < funcDecl .Body .Pos () || pos >= funcDecl .Body .End () {
176+ return true
177+ }
178+ // Must be a method (has a receiver) with a recognized marshaler name.
179+ if funcDecl .Recv != nil && funcDecl .Recv .NumFields () > 0 {
180+ if customMarshalerMethods [funcDecl .Name .Name ] {
181+ found = true
182+ }
183+ }
184+ return false
185+ })
186+
187+ return found
188+ }
189+
190+ // typeImplementsMarshaler reports whether typ (or its element type for
191+ // containers) has a method with the given name, indicating it implements a
192+ // custom marshaler interface (e.g. json.Marshaler). When a type has a custom
193+ // marshaler, the serialization library calls that method instead of serializing
194+ // fields directly, making struct field analysis irrelevant.
195+ func typeImplementsMarshaler (typ types.Type , methodName string ) bool {
196+ if methodName == "" {
197+ return false
198+ }
199+ named := elementNamedType (typ )
200+ if named == nil {
201+ return false
202+ }
203+ // Check both value and pointer receiver methods via the pointer method set,
204+ // which is a superset of the value method set.
205+ mset := types .NewMethodSet (types .NewPointer (named ))
206+ for i := 0 ; i < mset .Len (); i ++ {
207+ if mset .At (i ).Obj ().Name () == methodName {
208+ return true
209+ }
210+ }
211+ return false
212+ }
213+
214+ // elementNamedType unwraps pointers, slices, arrays, and maps to find the
215+ // innermost Named type. Returns nil if no Named type is found.
216+ func elementNamedType (typ types.Type ) * types.Named {
217+ switch t := typ .(type ) {
218+ case * types.Named :
219+ return t
220+ case * types.Pointer :
221+ return elementNamedType (t .Elem ())
222+ case * types.Slice :
223+ return elementNamedType (t .Elem ())
224+ case * types.Array :
225+ return elementNamedType (t .Elem ())
226+ case * types.Map :
227+ return elementNamedType (t .Elem ())
122228 }
229+ return nil
230+ }
123231
124- return nil , nil
232+ // compositeLitFieldIsTransformed checks whether expr is a composite literal
233+ // in which the given field name is assigned a function call result. A function
234+ // call indicates the value is being transformed (e.g. masked or redacted)
235+ // before serialization.
236+ func compositeLitFieldIsTransformed (expr ast.Expr , fieldName string ) bool {
237+ // Unwrap address-of operator: &Struct{...}
238+ if unary , ok := expr .(* ast.UnaryExpr ); ok {
239+ expr = unary .X
240+ }
241+ lit , ok := expr .(* ast.CompositeLit )
242+ if ! ok {
243+ return false
244+ }
245+ for _ , elt := range lit .Elts {
246+ kv , ok := elt .(* ast.KeyValueExpr )
247+ if ! ok {
248+ continue
249+ }
250+ ident , ok := kv .Key .(* ast.Ident )
251+ if ! ok || ident .Name != fieldName {
252+ continue
253+ }
254+ _ , isCall := kv .Value .(* ast.CallExpr )
255+ return isCall
256+ }
257+ return false
125258}
126259
127260func isNamedTypeInPackage (typ types.Type , pkgPath , typeName string ) bool {
@@ -382,7 +515,7 @@ func (r *secretSerialization) findSensitiveSerializedField(st *types.Struct, tag
382515 }
383516
384517 if gosec .RegexMatchWithCache (r .pattern , field .Name ()) || gosec .RegexMatchWithCache (r .pattern , effectiveKey ) {
385- return sensitiveFieldMatch {fieldName : field .Name (), jsonKey : effectiveKey , found : true }
518+ return sensitiveFieldMatch {fieldName : field .Name (), serializedKey : effectiveKey , found : true }
386519 }
387520 }
388521
0 commit comments