GHSA-rf2f-x2p8-fq45: 
Analyse et atténuation de la vulnérabilité

Summary

When an application registers Go native types with ext.NativeTypes(..., ext.ParseStructTag("json")) and serializes a CEL result to Protobuf JSON (structpb.Value / structpb.Struct), cel-go emits the promoted subfields of an embedded struct even when that embedded field is hidden from JSON with the json:"-" tag. The Go standard library encoding/json omits such fields entirely. An application that relies on json:"-" to keep secrets (for example an embedded credentials struct) out of serialized output therefore leaks those fields to whoever receives the CEL result. In general, it is uncommon to use such objects with CEL, and a best practice is to keep the CEL attribute types sanitized and separate from the rest of the application. While it is possible to trigger this case, good process and clean separation of concerns will reduce the likelihood to nearly zero.

Root Cause

ParseStructTag("json") documents encoding/json field-name semantics, and cel-go partially honors the json:"-" skip sentinel: a leaf field tagged json:"-", or an embedded field tagged json:"-" considered by itself, is excluded from the serialized field set. The exclusion is name based (or leaf-tag based) and does not account for promotion. When a struct embeds another struct and hides the embed with json:"-", Go promotes the embedded struct's exported subfields to the outer type. Those promoted subfields carry their own real names and no json tag of their own, so they are neither recognized as the skip sentinel nor suppressed by the embed's tag. They enter the serialized field set and are written to the output. encoding/json, by contrast, drops a --tagged embedded field before promotion, so none of its subfields are emitted.

common/types/native.go
  - newNativeType: builds the serialized field set. In the released code it iterates
    reflect.VisibleFields and drops a field only when its resolved name is the skip
    sentinel "-" (isSkippedFieldName). Promoted subfields of a json:"-"-tagged embed
    resolve to their real Go names, are not "-", and are added to the field set.
  - (*nativeObj).ConvertToNative: the jsonStructType case iterates that field set and
    writes each field into structpb.Struct.Fields, emitting the promoted subfields.

Impact

An application is affected when it (a) registers native types with ParseStructTag("json"), (b) serializes CEL results to structpb.Value/structpb.Struct (the representation used whenever CEL output is rendered to Protobuf JSON), and (c) hides sensitive state by embedding a struct tagged json:"-". Under these conditions, fields the developer intended to keep out of JSON (for example passwords, API keys, internal tokens) are emitted in the serialized result and disclosed to its recipient. The exposure is confidentiality only; the attacker gains no write or availability primitive from this defect. Exploitation is passive: the attacker observes serialized output rather than sending a crafted payload, so it depends on the affected data and configuration already being present.

Proof of Concept

Dependencies: Go 1.25.x and the affected module. Build with a vendored or module-resolved dependency set. Save the program in a module that requires cel.dev/cel-go at v0.32.0 and run it.

package main
import (
	"encoding/json"
	"fmt"
	"reflect"
	"cel.dev/cel-go/cel"
	"cel.dev/cel-go/ext"
	structpb "google.golang.org/protobuf/types/known/structpb"
)
type Secrets struct {
	Password string
	APIKey   string
}
// The embedded Secrets struct is hidden from JSON with json:"-".
type Config struct {
	Secrets `json:"-"`
	Name    string
}
func celJSON(v any) string {
	env, _ := cel.NewEnv(ext.NativeTypes(reflect.TypeOf(v), ext.ParseStructTag("json")))
	rv := env.CELTypeAdapter().NativeToValue(v)
	out, err := rv.ConvertToNative(reflect.TypeOf(&structpb.Value{}))
	if err != nil {
		return "ERROR: " + err.Error()
	}
	b, _ := out.(*structpb.Value).MarshalJSON()
	return string(b)
}
func main() {
	c := Config{Secrets{"hunter2", "sk-DEAD"}, "prod"}
	ej, _ := json.Marshal(c)
	fmt.Printf("encoding/json = %s\n", string(ej))
	fmt.Printf("cel-go        = %s\n", celJSON(c))
}
encoding/json = {"Name":"prod"}
cel-go        = {"APIKey":"sk-DEAD", "Name":"prod", "Password":"hunter2"}

The same program with the secret placed on a non-embedded leaf field tagged json:"-" produces cel-go = {"Public":"p"}, confirming that the leaf case is correctly hidden and that the embedded promotion path is the specific gap.

Attack Chain

  1. Exposure: The application uses the public, shipped ext.NativeTypes(...) API with ParseStructTag("json") and serializes CEL results to structpb Protobuf JSON, the standard path for rendering CEL output as JSON to a consumer.
  2. Control: The developer places sensitive values in a struct that is embedded into a CEL-registered type and hides that embed with json:"-", expecting encoding/json omission semantics that ParseStructTag("json") advertises.
  3. Path: Registration builds the serialized field set in newNativeType; converting the value to structpb reaches the jsonStructType case of (*nativeObj).ConvertToNative.
  4. Guard: The name-based skip drops fields whose resolved name is -, but promoted subfields of the json:"-"-tagged embed keep their real names and no tag, so the skip does not exclude them.
  5. Primitive: The promoted subfields are written into structpb.Struct.Fields and serialized.
  6. Result: Developer-designated-private fields (for example Password, APIKey) are disclosed in the serialized CEL output to its recipient, while encoding/json omits them.

Bypass Evidence

This is an incomplete-fix follow-up to the published advisory GHSA-gcjh-h69q-9w9g ("JSON Private Fields Exposed via NativeTypes and ParseStructTag"), which accepted this exact class (CWE information exposure of json:"-" fields via NativeTypes + ParseStructTag, including the ConvertToNative(jsonStructType) serialization sink). The released remediation added a name-based skip so that a field whose resolved name is the - sentinel is excluded. Reproduction confirms that remediation blocks the leaf json:"-" case (a leaf secret is correctly hidden) but is bypassed by the embedded-field shape: promoted subfields of a json:"-"-tagged embed carry their real names, so the name-based skip never matches them and they are serialized. Disproof attempts performed and their outcomes:

  • Tested the "works as designed" refutation (that ParseStructTag governs only field naming, not omission). Rejected: the project already treats this json:"-" exposure via the structpb serialization path as a vulnerability, and it partially honors json:"-" (leaf fields are hidden), so honoring the skip is intended and the embedded case is an inconsistent gap, not a design stance.
  • Verified the leak on the latest released artifact v0.32.0 under ParseStructTag("json"), not only on the development branch, to rule out a development-only regression.
  • Confirmed the control case (leaf json:"-" correctly hidden) in the same run, proving the released skip is effective for the leaf shape and the defect is specific to embedded-field promotion.
  • Confirmed the embedded promotion also leaks with a pointer embed (*Secrets tagged json:"-").

Affected Versions

  • Ecosystem: go
  • Package: cel.dev/cel-go
  • Confirmed affected range: <= 0.32.0
  • Latest release checked: v0.32.0 (Go module proxy / GitHub release, 2026-08-19)
  • Fix status: not fixed in any released version The module coordinate changed from github.com/google/cel-go to cel.dev/cel-go; v0.32.0 is the release published under the current cel.dev/cel-go path and is the latest release, and it is affected. The name-based skip that fixes the leaf json:"-" case is present from the v0.31.0 refactor onward, but the embedded-field promotion shape is not covered in any released version. Earlier releases under the prior module path expose the same class through a different serialized shape. No released version omits the embedded subfields, so no fixed release is established.

Suggested Fix

Enforce encoding/json omission semantics for the skip sentinel across promotion: when a serialized field is a subfield promoted from an embedded field whose configured tag (json or the tag chosen via ParseStructTag) is the skip sentinel -, exclude that subfield from both the CEL serialized field set (built in newNativeType) and the ConvertToNative(jsonStructType) output, mirroring the standard library's pre-promotion omission of --tagged embeds. Apply the same handling consistently for the other tag families the option supports (for example xml, yaml, bson). As an interim mitigation without a code change, applications should avoid relying on json:"-" on an embedded field to hide secrets from serialized CEL output, and should instead exclude sensitive data from CEL-registered types or serialize through a view type that contains only intended fields.

Reported by zx (GitHub: @manus-pi).


Source: NVD

Évaluation gratuite des vulnérabilités

Évaluez votre posture de sécurité cloud

Évaluez vos pratiques de sécurité cloud dans 9 domaines de sécurité afin d’évaluer votre niveau de risque et d’identifier les lacunes dans vos défenses.

Évaluation de la demande

Ressources Wiz supplémentaires

Obtenez une démo personnalisée

Prêt(e) à voir Wiz en action ?

"La meilleure expérience utilisateur que j’ai jamais vue, offre une visibilité totale sur les workloads cloud."
David EstlickRSSI
"Wiz fournit une interface unique pour voir ce qui se passe dans nos environnements cloud."
Adam FletcherChef du service de sécurité
"Nous savons que si Wiz identifie quelque chose comme critique, c’est qu’il l’est réellement."
Greg PoniatowskiResponsable de la gestion des menaces et des vulnérabilités