GHSA-gcjh-h69q-9w9g
cel-go: JSON Private Fields Exposed via NativeTypes and ParseStructTag
Summary
The function `ext.NativeTypes(ParseStructTag("json"))` does not honour the `encoding/json` skip directive `json:"-"`. Fields tagged `json:"-"` are registered in the CEL type system under the literal name `"-"` and are readable from any user-submitted CEL expression via `dyn(obj)["-"]`. Additionally, `newNativeTypes` silently registers every nested struct reachable from the type passed to `NativeTypes`, including types from third-party dependencies the developer never examined. ## Root cause In `fieldNameByTag`, the helper used by `ParseStructTag("json")` to translate Go struct tags into CEL field names. See at `ext/native.go:146`: ```go func fieldNameByTag(structTagToParse string) func(field reflect.StructField) string { return func(field reflect.StructField) string { tag, found := field.Tag.Lookup(structTagToParse) if found { splits := strings.Split(tag, ",") if len(splits) > 0 { // We make the assumption that the leftmost entry in the tag is the name. // This seems to be true for most tags that have the concept of a name/key, such as: // https://pkg.go.dev/encoding/xml#Marshal // https://pkg.go.dev/encoding/json#Marshal // https://pkg.go.dev/go.mongodb.org/mongo-driver/bson#hdr-Structs // https://pkg.go.dev/go.yaml.in/yaml/v3#Marshal name := splits[0] return name } } return field.Name } } ``` For a field tagged `json:"-"`, this code splits the tag into `[]string{"-"}` and returns `"-"` as the CEL field name. It never checks whether `"-"` is the JSON skip sentinel. This contradicts the `encoding/json` rule that the source comment explicitly points readers to: ```text As a special case, if the field tag is "-", the field is always omitted. Note that a field with name "-" can still be generated using the tag "-,". ``` The public option also documents JSON-style parsing as the intended behavior. See at `ext/native.go:190`: ```go // ParseStructTag configures the struct tag to parse. The 0th item in the tag is used as the name of the CEL field. // For example: // If the tag to parse is "cel" and the struct field has tag cel:"foo", the CEL struct field will be "foo". // If the tag to parse is "json" and the struct field has tag json:"foo,omitempty", the CEL struct field will be "foo". func ParseStructTag(tag string) NativeTypesOption { return func(ntp *nativeTypeOptions) error { ntp.fieldNameHandler = fieldNameByTag(tag) return nil } } ``` A developer using `ParseStructTag("json")` is therefore led to expect `encoding/json` field-name semantics. Instead, `json:"-"` is treated as a real field name. The bad name is accepted during native type construction. `newNativeType` checks for duplicate field names, but it does not reject or skip empty names or skip sentinels. See at `ext/native.go:663`: ```go if fieldNameHandler != nil { fieldNames := make(map[string]struct{}) for idx := 0; idx < refType.NumField(); idx++ { field := refType.Field(idx) fieldName := toFieldName(fieldNameHandler, field) if _, found := fieldNames[fieldName]; found { return nil, fmt.Errorf("invalid field name `%s` in struct `%s`: %w", fieldName, refType.Name(), errDuplicatedFieldName) } else { fieldNames[fieldName] = struct{}{} } } } ``` Once accepted, the field becomes part of CEL's view of the type. Field enumeration reports it as a normal field name. See at `ext/native.go:286`: ```go func (tp *nativeTypeProvider) FindStructFieldNames(typeName string) ([]string, bool) { if t, found := tp.nativeTypes[typeName]; found { fieldCount := t.refType.NumField() fields := make([]string, fieldCount) for i := 0; i < fieldCount; i++ { fields[i] = toFieldName(tp.options.fieldNameHandler, t.refType.Field(i)) }
Affected packages
| Ecosystem | Package | Affected versions | Fixed versions |
|---|---|---|---|
| Go | github.com/google/cel-go | — | 0.29.0 |
Remediation: Upgrade to 0.29.0 or later.
References
Includes data from the GitHub Advisory Database, licensed under CC-BY 4.0.