
PEACH
Un cadre d’isolation des locataires
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.
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.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.
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.
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.json:"-", expecting encoding/json omission semantics that ParseStructTag("json") advertises.newNativeType; converting the value to structpb reaches the jsonStructType case of (*nativeObj).ConvertToNative.-, but promoted subfields of the json:"-"-tagged embed keep their real names and no tag, so the skip does not exclude them.structpb.Struct.Fields and serialized.Password, APIKey) are disclosed in the serialized CEL output to its recipient, while encoding/json omits them.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:
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.v0.32.0 under ParseStructTag("json"), not only on the development branch, to rule out a development-only regression.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.*Secrets tagged json:"-").Ecosystem: goPackage: cel.dev/cel-goConfirmed affected range: <= 0.32.0Latest 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.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 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.
Obtenez une démo personnalisée
"La meilleure expérience utilisateur que j’ai jamais vue, offre une visibilité totale sur les workloads cloud."
"Wiz fournit une interface unique pour voir ce qui se passe dans nos environnements cloud."
"Nous savons que si Wiz identifie quelque chose comme critique, c’est qu’il l’est réellement."