Skip to content

Commit 355cfa5

Browse files
ccojocarclaude
andauthored
fix: reduce G117 false positives for custom marshalers and transformed values (#1614) (#1615)
G117 now skips findings when: - The marshal call is inside a custom marshaler method (MarshalJSON, MarshalYAML, etc.) - The type being marshaled implements a custom marshaler interface - A composite literal wraps the sensitive field value in a function call (e.g. mask()) Also fixes the issue message to show the correct format name (JSON/YAML/XML/TOML) instead of always saying "JSON key". Closes #1614 Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 744bfb5 commit 355cfa5

File tree

2 files changed

+366
-19
lines changed

2 files changed

+366
-19
lines changed

rules/secret_serialization.go

Lines changed: 152 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ type secretSerialization struct {
2121
}
2222

2323
type 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

3031
type functionSink struct {
@@ -44,15 +45,16 @@ type typeAnalysisCacheKey struct {
4445
}
4546

4647
type sensitiveFieldMatch struct {
47-
fieldName string
48-
jsonKey string
49-
found bool
48+
fieldName string
49+
serializedKey string
50+
found bool
5051
}
5152

5253
var 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

127260
func 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

Comments
 (0)