Skip to content

Update module github.com/google/cel-go to v0.30.0 [SECURITY] (main) - #3693

Open
red-hat-konflux[bot] wants to merge 1 commit into
mainfrom
konflux/mintmaker/main-main/go-github.com-google-cel-go-vulnerability
Open

red-hat-konflux[bot] wants to merge 1 commit into
mainfrom
konflux/mintmaker/main-main/go-github.com-google-cel-go-vulnerability

Conversation

@red-hat-konflux

Copy link
Copy Markdown
Contributor

This PR contains the following updates:

Package Change Age Confidence
github.com/google/cel-go v0.28.0 → v0.30.0 age confidence

cel-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 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:

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:

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".
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:

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:

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))
        }
        return fields, true
    }
    if celTypeFields, found := tp.baseProvider.FindStructFieldNames(typeName); found {
        return celTypeFields, true
    }
    return tp.baseProvider.FindStructFieldNames(typeName)
}

Field lookup also treats the name as valid and returns the underlying Go field value.

See at ext/native.go:303:

func (tp *nativeTypeProvider) FindStructFieldType(typeName, fieldName string) (*types.FieldType, bool) {
    t, found := tp.nativeTypes[typeName]
    if !found {
        return tp.baseProvider.FindStructFieldType(typeName, fieldName)
    }
    refField, isDefined := t.hasField(fieldName)
    if !found || !isDefined {
        return nil, false
    }

    return &types.FieldType{
        IsSet: func(obj any) bool {
            refVal := reflect.Indirect(reflect.ValueOf(obj))
            refField := refVal.FieldByName(refField.Name)
            return !refField.IsZero()
        },
        GetFrom: func(obj any) (any, error) {
            refVal := reflect.Indirect(reflect.ValueOf(obj))
            refField := refVal.FieldByName(refField.Name)
            return getFieldValue(refField), nil
        },
    }, true
}

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.

See at ext/native.go:501:

case jsonStructType:
    refVal := reflect.Indirect(o.refValue)
    refType := refVal.Type()
    fields := make(map[string]*structpb.Value, refVal.NumField())
    for i := 0; i < refVal.NumField(); i++ {
        fieldType := refType.Field(i)
        fieldValue := refVal.Field(i)
        if !fieldValue.IsValid() || fieldValue.IsZero() {
            continue
        }
        fieldName := toFieldName(o.valType.fieldNameHandler, fieldType)
        fieldCELVal := o.NativeToValue(fieldValue.Interface())
        fieldJSONVal, err := fieldCELVal.ConvertToNative(jsonValueType)
        if err != nil {
            return nil, err
        }
        fields[fieldName] = fieldJSONVal.(*structpb.Value)
    }
    return &structpb.Struct{Fields: fields}, nil

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:

func newNativeTypes(fieldNameHandler NativeTypesFieldNameHandler, rawType reflect.Type) ([]*nativeType, error) {
    nt, err := newNativeType(fieldNameHandler, rawType)
    if err != nil {
        return nil, err
    }
    result := []*nativeType{nt}

    var iterateStructMembers func(reflect.Type)
    iterateStructMembers = func(t reflect.Type) {
        if k := t.Kind(); k == reflect.Pointer || k == reflect.Slice || k == reflect.Array || k == reflect.Map {
            iterateStructMembers(t.Elem())
            return
        }
        if t.Kind() != reflect.Struct {
            return
        }

        nt, ntErr := newNativeType(fieldNameHandler, t)
        if ntErr != nil {
            err = ntErr
            return
        }
        result = append(result, nt)

        for idx := 0; idx < t.NumField(); idx++ {
            iterateStructMembers(t.Field(idx).Type)
        }
    }
    iterateStructMembers(rawType)

    return result, err
}

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.
type AuthCtx struct {
    UserID string `json:"userId"`
    Secret string `json:"-"` // server-internal; never appears in JSON output
}

// Developer registers only this type.
type Req struct{ Auth AuthCtx `json:"auth"` }

func main() {
    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"
)

type AuthCtxConv struct {
    UserID string `json:"userId"`
    Secret string `json:"-"` // should never appear in JSON output
}

type ReqConv struct{ Auth AuthCtxConv `json:"auth"` }

func main() {
    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)
    if v, 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.

Severity

  • CVSS Score: 6.3 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N

References

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.0

Compare Source

v0.29.2

Compare Source

v0.29.1

Compare Source

v0.29.0

Compare Source

v0.28.1

Compare Source


Configuration

📅 Schedule: (UTC)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • At any time (no schedule defined)

🚦 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.


  • If you want to rebase/retry this PR, check this box

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.

Signed-off-by: red-hat-konflux <126015336+red-hat-konflux[bot]@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

No actionable comments were generated in the recent review.

Walkthrough

The indirect github.com/google/cel-go dependency was upgraded from v0.28.0 to v0.30.0 in the root, acceptance, and tools Go modules.

Changes: cel-go Dependency Upgrade

Layer / File(s) Summary
Update cel-go module requirements
go.mod, acceptance/go.mod, tools/go.mod
Each module updates the indirect github.com/google/cel-go requirement from v0.28.0 to v0.30.0.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Change: Other

Suggested reviewers: simonbaird, robnester-rh, jsmid1

Merge Risk: Minimal · up to 2090e

This dependency bump moves cel-go to a release past the advisory's patched version. No actionable merge risk remains.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed 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 Passed 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 Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed 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.

@fullsend-ai-review fullsend-ai-review Bot added the risk/moderate PR risk: moderate label Nov 12, 2026
@fullsend-ai-review

Copy link
Copy Markdown

<!-- fullsend:risk-assessment -->
Risk Assessment: moderate (2/5)

Details

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.

@fullsend-ai-review

Copy link
Copy Markdown

Looks good to me


Labels: Dependency file update in Go modules warrants the dependencies and go labels.

<!-- fullsend:review-findings-v2:eyJ2ZXJzaW9uIjoyLCJmaW5kaW5ncyI6W119 -->

@fullsend-ai-review fullsend-ai-review Bot added ready-for-merge All reviewers approved — ready to merge dependencies Pull requests that update a dependency file go Pull requests that update Go code labels Nov 12, 2026
@codecov

codecov Bot commented Nov 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Flag Coverage Δ
acceptance 54.61% <ø> (ø)
generative 12.22% <ø> (ø)
integration 23.62% <ø> (ø)
unit 72.29% <ø> (ø)

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file go Pull requests that update Go code ready-for-merge All reviewers approved — ready to merge renovate risk/moderate PR risk: moderate security size: XS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants