Files
gosec/rules/secret_serialization.go
oittaa 0f6f21cb3f feat: add secrets serialization G117 (#1451)
* Rule to detect secrets serialization

* Add G117 to rules_test.go

* Fix false positives

* Map to CWE 499, update README
2026-01-04 17:21:22 +02:00

124 lines
3.0 KiB
Go

package rules
import (
"fmt"
"go/ast"
"reflect"
"regexp"
"strconv"
"strings"
"github.com/securego/gosec/v2"
"github.com/securego/gosec/v2/issue"
)
type secretSerialization struct {
issue.MetaData
pattern *regexp.Regexp
}
func (r *secretSerialization) ID() string {
return r.MetaData.ID
}
func (r *secretSerialization) Match(n ast.Node, ctx *gosec.Context) (*issue.Issue, error) {
field, ok := n.(*ast.Field)
if !ok || len(field.Names) == 0 {
return nil, nil // skip embedded (anonymous) fields
}
// Parse the JSON tag to determine behavior
omitted := false
jsonKey := ""
if field.Tag != nil {
if tagVal, err := strconv.Unquote(field.Tag.Value); err == nil && tagVal != "" {
st := reflect.StructTag(tagVal)
if tag := st.Get("json"); tag != "" {
if tag == "-" {
omitted = true
} else {
// "name,omitempty" -> "name"
// "-," -> "-" (A field literally named "-")
parts := strings.SplitN(tag, ",", 2)
jsonKey = parts[0]
}
}
}
}
if omitted {
return nil, nil
}
// Iterate over all names in this field definition
// e.g., type T struct { Pass, Salt string }
isSensitiveType := false
switch t := field.Type.(type) {
case *ast.Ident:
if t.Name == "string" {
isSensitiveType = true
}
case *ast.StarExpr:
if ident, ok := t.X.(*ast.Ident); ok && ident.Name == "string" {
isSensitiveType = true
}
case *ast.ArrayType:
if star, ok := t.Elt.(*ast.StarExpr); ok {
if ident, ok := star.X.(*ast.Ident); ok && ident.Name == "string" {
isSensitiveType = true // []*string
}
} else if ident, ok := t.Elt.(*ast.Ident); ok {
if ident.Name == "string" || ident.Name == "byte" {
isSensitiveType = true // []string or []byte
}
}
}
if !isSensitiveType {
return nil, nil
}
// Check each named exported field
for _, nameIdent := range field.Names {
fieldName := nameIdent.Name
if fieldName == "_" || !ast.IsExported(fieldName) {
continue
}
effectiveKey := jsonKey
if effectiveKey == "" {
effectiveKey = fieldName
}
if r.pattern.MatchString(fieldName) || r.pattern.MatchString(effectiveKey) {
msg := fmt.Sprintf("Exported struct field %q (JSON key %q) matches secret pattern", fieldName, effectiveKey)
return ctx.NewIssue(field, r.ID(), msg, r.Severity, r.Confidence), nil
}
}
return nil, nil
}
func NewSecretSerialization(id string, conf gosec.Config) (gosec.Rule, []ast.Node) {
patternStr := `(?i)\b((?:api|access|auth|bearer|client|oauth|private|refresh|session|jwt)[_-]?(?:key|secret|token)s?|password|passwd|pwd|pass|secret|cred|jwt)\b`
if val, ok := conf[id]; ok {
if m, ok := val.(map[string]interface{}); ok {
if p, ok := m["pattern"].(string); ok && p != "" {
patternStr = p
}
}
}
return &secretSerialization{
pattern: regexp.MustCompile(patternStr),
MetaData: issue.MetaData{
ID: id,
What: "Exported struct field appears to be a secret and is not ignored by JSON marshaling",
Severity: issue.Medium,
Confidence: issue.Medium,
},
}, []ast.Node{(*ast.Field)(nil)}
}