GHSA-gcjh-h69q-9w9g cel-go: JSON Private Fields Exposed via NativeTypes and ParseStructTag
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 causeIn `fieldNameByTag`, the helper used by `ParseStructTag("json")` to translate Go struct tags into CEL field names.See at `ext/native.go:146`: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:The public option also documents JSON-style parsing as the intended behavior.
See at `ext/native.go:190`: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`: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`:Field lookup also treats the name as valid and returns the underlying Go field value.See at `ext/native.go:303`:At runtime, native objects advertise index access.
See at `ext/native.go:37`:Because `traits.IndexerType` is present, a user expression can bypass ordinary field syntax and read the registered `"-"` field with bracket access:The same mistaken name is also used when converting native objects to JSON-like CEL values. `ConvertToNative(jsonStructType)` iterates all Go struct fields, computes the CEL field name, and inserts it into the output map without applying the JSON skip rule.See at `ext/native.go:501`:This means a `json:"-"` secret is exposed in two ways: it can be read directly through CEL indexing as `dyn(obj)["-"]`, and it can appear under the key `"-"` in JSON struct conversion output.The blast radius is widened by `newNativeTypes`, which registers not only the type explicitly passed to `NativeTypes`, but also every nested struct reachable from its fields.See at `ext/native.go:609`:As a result, a developer can register one apparently safe request type while a nested dependency type is silently registered too. If that nested type contains a `json:"-"` secret, CEL still receives a readable field named `"-"` even though the developer never registered or audited that nested type directly.Reproduction**Expected:** expression compile error or empty result; `json:"-"` field should not be
accessible.  
**Actual:** `sk-live-s3cr3t`; the server-injected secret is returned verbatim.The same field is also included under key `"-"` in `ConvertToNative(jsonStructType)`
output, and appears in `FindStructFieldNames` enumeration.path 1. CEL indexingTested against the released module `github.com/google/cel-go v0.28.1`
(latest stable release as of 2026-05-12), using the `go.mod` entry:Running the PoC above (`go run main.go`) produces:The secret value is returned verbatim, with no error at compile time or at runtime.Path 2. `ConvertToNative(jsonStructType)`When the `nativeObj` for the `AuthCtx` value is converted to a Protobuf `Struct`
(the representation used whenever CEL output is serialised to JSON), the
`json:"-"` field appears in the output map under the key `"-"`.Running the PoC above produces:The `"-"` key is present in the serialised Protobuf struct alongside `userId`.
Any system that converts a CEL evaluation result to JSON (e.g. via `structpb.Struct`) will include the secret in the output, regardless of whether the `dyn()["-"]` indexing path is used.ImpactAny user who can submit CEL expressions to an application that uses `ext.NativeTypes(ParseStructTag("json"))` can read struct fields that the developer explicitly marked `json:"-"` to keep out of serialised output. By writing `dyn(obj)["-"]`, the attacker retrieves the raw Go field value, typically a secret, internal token, or private identifier, with no compile-time or runtime error. Because `newNativeTypes` silently registers every nested struct reachable from the root type, the attacker may also reach secrets in dependency types the developer never intended to expose to CEL.RemediationDo not treat `json:"-"` as a CEL field named `"-"`. Model it as an explicit skipped field, not as an empty string field name.Update the struct-tag parsing path so exact `json:"-"` returns “skip this field”, while `json:"-,"` continues to mean the literal field name `"-"`, matching `encoding/json` semantics.Apply that skip decision consistently anywhere native fields are exposed or resolved:duplicate-name validation in `newNativeType`field enumeration in `FindStructFieldNames`field type lookup in `FindStructFieldType`runtime lookup in `fieldByName` / `hasField`object construction in `NewValue`JSON conversion in `ConvertToNative(jsonStructType)`Apply the same omit handling for `xml:"-"`, `yaml:"-"`, and `bson:"-"` where `ParseStructTag` is used.
CVSS v4.0 score: 6.3