You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Update module github.com/google/cel-go to v0.30.0 [SECURITY] (main) - #3693
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:
funcfieldNameByTag(structTagToParsestring) func(field reflect.StructField) string {
returnfunc(field reflect.StructField) string {
tag, found:=field.Tag.Lookup(structTagToParse)
iffound {
splits:=strings.Split(tag, ",")
iflen(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#Marshalname:=splits[0]
returnname
}
}
returnfield.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:
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:
// 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".funcParseStructTag(tagstring) NativeTypesOption {
returnfunc(ntp*nativeTypeOptions) error {
ntp.fieldNameHandler=fieldNameByTag(tag)
returnnil
}
}
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:
iffieldNameHandler!=nil {
fieldNames:=make(map[string]struct{})
foridx:=0; idx<refType.NumField(); idx++ {
field:=refType.Field(idx)
fieldName:=toFieldName(fieldNameHandler, field)
if_, found:=fieldNames[fieldName]; found {
returnnil, 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.
At runtime, native objects advertise index access.
See at ext/native.go:37:
var (
nativeObjTraitMask=traits.FieldTesterType|traits.IndexerType
)
Because traits.IndexerType is present, a user expression can bypass ordinary field syntax and read the registered "-" field with bracket access:
dyn(req.auth)["-"]
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.
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.
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
package main
import (
"fmt""reflect""github.com/google/cel-go/cel""github.com/google/cel-go/ext"
)
// Simulates a library type; developer never registers this directly.typeAuthCtxstruct {
UserIDstring`json:"userId"`Secretstring`json:"-"`// server-internal; never appears in JSON output
}
// Developer registers only this type.typeReqstruct{ AuthAuthCtx`json:"auth"` }
funcmain() {
env, _:=cel.NewEnv(
// Only Req is passed; AuthCtx is registered silently by newNativeTypes.ext.NativeTypes(reflect.TypeOf(Req{}), ext.ParseStructTag("json")),
cel.Variable("req", cel.ObjectType("main.Req")),
)
ast, _:=env.Compile(`dyn(req.auth)["-"]`)
prg, _:=env.Program(ast)
out, _, _:=prg.Eval(map[string]any{
"req": Req{Auth: AuthCtx{UserID: "alice", Secret: "sk-live-s3cr3t"}},
})
fmt.Println(out) // sk-live-s3cr3t
}
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 indexing
Tested 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:
require github.com/google/cel-go v0.28.1
Running the PoC above (go run main.go) produces:
sk-live-s3cr3t
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 "-".
package main
import (
"encoding/json""fmt""reflect""github.com/google/cel-go/cel""github.com/google/cel-go/ext"
structpb "google.golang.org/protobuf/types/known/structpb"
)
typeAuthCtxConvstruct {
UserIDstring`json:"userId"`Secretstring`json:"-"`// should never appear in JSON output
}
typeReqConvstruct{ AuthAuthCtxConv`json:"auth"` }
funcmain() {
env, _:=cel.NewEnv(
ext.NativeTypes(reflect.TypeOf(ReqConv{}), ext.ParseStructTag("json")),
cel.Variable("req", cel.ObjectType("main.ReqConv")),
)
ast, _:=env.Compile(`req.auth`)
prg, _:=env.Program(ast)
out, _, _:=prg.Eval(map[string]any{
"req": ReqConv{Auth: AuthCtxConv{UserID: "alice", Secret: "sk-live-s3cr3t"}},
})
jsonStructType:=reflect.TypeOf(&structpb.Struct{})
raw, _:=out.ConvertToNative(jsonStructType)
st:=raw.(*structpb.Struct)
b, _:=json.MarshalIndent(st.AsMap(), "", " ")
fmt.Printf("ConvertToNative(jsonStructType) output:\n%s\n", b)
fmt.Printf("\nDirect field access via \"-\" key present: %v\n", st.Fields["-"] !=nil)
ifv, ok:=st.Fields["-"]; ok {
fmt.Printf("Value: %s\n", v.GetStringValue())
}
}
Running the PoC above produces:
ConvertToNative(jsonStructType) output:
{
"-": "sk-live-s3cr3t",
"userId": "alice"
}
Direct field access via "-" key present: true
Value: sk-live-s3cr3t
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.
Impact
Any 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.
Remediation
Do 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.
This dependency bump moves cel-go to a release past the advisory's patched version. No actionable merge risk remains.
Pre-merge checks: 4
Check name
Status
Explanation
Title check
The title names the change: updating github.com/google/cel-go to v0.30.0. The diff bumps this indirect dependency in go.mod, acceptance/go.mod and tools/go.mod. The title also tags the change as [SECU…
Description check
The description follows the Renovate dependency update format. It lists the package and version change (v0.28.0 to v0.30.0), the advisory details (GHSA-gcjh-h69q-9w9g / GO-2026-6094) with root cause, …
Linked Issues check
Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check
Check skipped because no linked issues were found for this pull request.
Full details: Title check
Explanation
The title names the change: updating github.com/google/cel-go to v0.30.0. The diff bumps this indirect dependency in go.mod, acceptance/go.mod and tools/go.mod. The title also tags the change as [SECURITY], which matches the GHSA-gcjh-h69q-9w9g advisory the description cites. The title is specific and accurate.
Full details: Description check
Explanation
The description follows the Renovate dependency update format. It lists the package and version change (v0.28.0 to v0.30.0), the advisory details (GHSA-gcjh-h69q-9w9g / GO-2026-6094) with root cause, reproduction, impact and remediation, plus severity and references. It does not follow the repository template. It has no '#### What:', '#### Why:' or '#### Tickets:' sections. It also does not state any validation results for v0.30.0 itself. The PoC was run against v0.28.1, not v0.30.0. The content is substantive, so the check passes, but the template sections are missing.
Warning
Some tools did not complete. Review the errors below.
OSV Scanner (2.7.0)
go.mod
OSV Scanner failed without producing a report
Recent review info
Run configuration
Source mode: Auto
Repository: Unavailable
Branch: Unavailable
File: No configuration file from this repository applied
Commit: Unavailable
Layers: Organization UI
Review profile: CHILL
Plan: Enterprise
Run ID: 4f8d1625-bf7a-4947-8267-86ea570391aa
Commits
Reviewing files that changed from the base of the PR and between 3bc55d6 and 2090e8c.
Files ignored due to path filters: 3
acceptance/go.sum is excluded by !**/*.sum
go.sum is excluded by !**/*.sum
tools/go.sum is excluded by !**/*.sum
Files selected for processing: 3
acceptance/go.mod
go.mod
tools/go.mod
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 8 remain after this review.
Bot-authored (Renovate) security dependency bump of cel-go across three go.mod/go.sum pairs; XS in size (18 lines) with no CI, protected-path, or security-sensitive file changes, yielding a moderate score driven primarily by the multi-file dependency update and zero test-file ratio, both expected artifacts of automated dependency management.
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
This branch has not been deployed
No deployments
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR contains the following updates:
v0.28.0→v0.30.0cel-go: JSON Private Fields Exposed via NativeTypes and ParseStructTag
GHSA-gcjh-h69q-9w9g / GO-2026-6094
More information
Details
The function
ext.NativeTypes(ParseStructTag("json"))does not honour theencoding/jsonskip directivejson:"-". Fields taggedjson:"-"are registered in the CEL type system under the literal name"-"and are readable from any user-submitted CEL expression viadyn(obj)["-"].Additionally,
newNativeTypessilently registers every nested struct reachable from the type passed toNativeTypes, including types from third-party dependencies the developer never examined.Root cause
In
fieldNameByTag, the helper used byParseStructTag("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/jsonrule 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 expectencoding/jsonfield-name semantics. Instead,json:"-"is treated as a real field name.The bad name is accepted during native type construction.
newNativeTypechecks 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.IndexerTypeis 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 asdyn(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 toNativeTypes, 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 beaccessible.
Actual:
sk-live-s3cr3t; the server-injected secret is returned verbatim.The same field is also included under key
"-"inConvertToNative(jsonStructType)output, and appears in
FindStructFieldNamesenumeration.path 1. CEL indexing
Tested against the released module
github.com/google/cel-go v0.28.1(latest stable release as of 2026-05-12), using the
go.modentry: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
nativeObjfor theAuthCtxvalue is converted to a ProtobufStruct(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 alongsideuserId.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 thedyn()["-"]indexing path is used.Impact
Any user who can submit CEL expressions to an application that uses
ext.NativeTypes(ParseStructTag("json"))can read struct fields that the developer explicitly markedjson:"-"to keep out of serialised output. By writingdyn(obj)["-"], the attacker retrieves the raw Go field value, typically a secret, internal token, or private identifier, with no compile-time or runtime error. BecausenewNativeTypessilently 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.Remediation
Do 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”, whilejson:"-,"continues to mean the literal field name"-", matchingencoding/jsonsemantics.Apply that skip decision consistently anywhere native fields are exposed or resolved:
newNativeTypeFindStructFieldNamesFindStructFieldTypefieldByName/hasFieldNewValueConvertToNative(jsonStructType)Apply the same omit handling for
xml:"-",yaml:"-", andbson:"-"whereParseStructTagis used.Severity
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:NReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
JSON private fields exposed via NativeTypes and ParseStructTag in github.com/google/cel-go
GHSA-gcjh-h69q-9w9g / GO-2026-6094
More information
Details
JSON private fields exposed via NativeTypes and ParseStructTag in github.com/google/cel-go
Severity
Unknown
References
This data is provided by OSV and the Go Vulnerability Database (CC-BY 4.0).
Release Notes
google/cel-go (github.com/google/cel-go)
v0.30.0Compare Source
v0.29.2Compare Source
v0.29.1Compare Source
v0.29.0Compare Source
v0.28.1Compare Source
Configuration
📅 Schedule: (UTC)
🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.
♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about this update again.
To execute skipped test pipelines write comment
/ok-to-test.Documentation
Find out how to configure dependency updates in MintMaker documentation or see all available configuration options in Renovate documentation.