Summary
Cedar's JSON formats (entity escapes, policy AST, schema, entity data, request) are case-sensitive in the Rust reference implementation, but Go's encoding/json performs case-insensitive matching of JSON keys to struct field tags by default. As a result, cedar-go accepts JSON inputs with miscased keys that the Rust implementation rejects, producing cross-implementation divergence and, in some cases, ambiguous parses.
Scope
The root cause exists wherever a json:"..." struct tag is decoded. Highest-impact sites:
-
Entity / extension escapes — types/json.go:25-38
__entity, __extn, fn, arg, type, id
- Symptom: cedar-go emits EntityUIDs or extension functions where the Rust implementation emits Records.
-
Cedar JSON policy AST — internal/json/json.go
- Value markers (capitalized by spec):
Value (88), Var (91), Set (134), Record (137). The Cedar spec uses capitalization here as a discriminator against lowercase operators at the same level. {"value": ...}, {"set": [...]}, {"record": {...}} should be rejected.
- camelCase operators:
isEmpty, containsAll, containsAny, getTag, hasTag, if-then-else, plus neg, has, is, like, in, contains.
- Policy structure keys:
effect, principal, action, resource, conditions, annotations, op, entity, entities, entity_type, kind, body, left, right, arg, attr, pattern, if, then, else.
- Note:
nodeJSON.UnmarshalJSON (internal/json/json_unmarshal.go:274) already calls DisallowUnknownFields(). This is not sufficient — DisallowUnknownFields rejects unknown keys but still performs case-insensitive matching for known fields. Miscasings of recognized tags pass through silently.
-
Schema JSON — x/exp/schema/internal/json/json.go
entityTypes, commonTypes, memberOfTypes, memberOf, appliesTo, principalTypes, resourceTypes, shape, tags, attributes, element, enum, required, annotations.
-
Entity JSON — types/entity.go:11-14, 33-36
uid, parents, attrs, tags.
-
Request / Authorize JSON — types/authorize.go:8-11, 47-84
principal, action, resource, context, reasons, errors, policy, position, message, filename, offset, line, column.
Impact
This issue can potentially cause cedar-go and the Rust implementation to produce different authorization decisions.
However, the likelihood of exploitation by a malicious requester is low. The application would have to accept a JSON payload directly from a malicious requester as part of the authorization context, which seems highly unlikely.
Possible fixes
Enforce case sensitivity with a common helper function
Introduce a single helper that enforces exact-case key matching, then route every Cedar-spec JSON decode through it:
// strictUnmarshal decodes b into v but rejects any JSON key whose case does
// not exactly match a `json:"..."` tag on v (or, for nested structs,
// recursively on their tags).
func strictUnmarshal(b []byte, v any) error { ... }
Implementation sketch: parse b into map[string]json.RawMessage, reflect over v's tag set to build the set of valid exact-case keys at this level, reject any input key not present in that set, then re-emit only the validated keys and json.Unmarshal into v. Recurse for fields that are themselves structs. The cost is one extra pass per decode; for Cedar JSON payloads this is negligible.
Migrate to json/v2
Unfortunately, Go's json v2 package, which enforces case sensitivity (amongst other niceties) still hasn't stabilized. However, we could wait to fix this bug until such time as it has.
Backwards compatibility
This is a tightening change: inputs that previously parsed or passed schema validation will now error in conformance with the Rust implementation. There is no desire to preserve backwards compatibility.
Summary
Cedar's JSON formats (entity escapes, policy AST, schema, entity data, request) are case-sensitive in the Rust reference implementation, but Go's
encoding/jsonperforms case-insensitive matching of JSON keys to struct field tags by default. As a result, cedar-go accepts JSON inputs with miscased keys that the Rust implementation rejects, producing cross-implementation divergence and, in some cases, ambiguous parses.Scope
The root cause exists wherever a
json:"..."struct tag is decoded. Highest-impact sites:Entity / extension escapes —
types/json.go:25-38__entity,__extn,fn,arg,type,idCedar JSON policy AST —
internal/json/json.goValue(88),Var(91),Set(134),Record(137). The Cedar spec uses capitalization here as a discriminator against lowercase operators at the same level.{"value": ...},{"set": [...]},{"record": {...}}should be rejected.isEmpty,containsAll,containsAny,getTag,hasTag,if-then-else, plusneg,has,is,like,in,contains.effect,principal,action,resource,conditions,annotations,op,entity,entities,entity_type,kind,body,left,right,arg,attr,pattern,if,then,else.nodeJSON.UnmarshalJSON(internal/json/json_unmarshal.go:274) already callsDisallowUnknownFields(). This is not sufficient —DisallowUnknownFieldsrejects unknown keys but still performs case-insensitive matching for known fields. Miscasings of recognized tags pass through silently.Schema JSON —
x/exp/schema/internal/json/json.goentityTypes,commonTypes,memberOfTypes,memberOf,appliesTo,principalTypes,resourceTypes,shape,tags,attributes,element,enum,required,annotations.Entity JSON —
types/entity.go:11-14, 33-36uid,parents,attrs,tags.Request / Authorize JSON —
types/authorize.go:8-11, 47-84principal,action,resource,context,reasons,errors,policy,position,message,filename,offset,line,column.Impact
This issue can potentially cause cedar-go and the Rust implementation to produce different authorization decisions.
However, the likelihood of exploitation by a malicious requester is low. The application would have to accept a JSON payload directly from a malicious requester as part of the authorization context, which seems highly unlikely.
Possible fixes
Enforce case sensitivity with a common helper function
Introduce a single helper that enforces exact-case key matching, then route every Cedar-spec JSON decode through it:
Implementation sketch: parse
bintomap[string]json.RawMessage, reflect overv's tag set to build the set of valid exact-case keys at this level, reject any input key not present in that set, then re-emit only the validated keys andjson.Unmarshalintov. Recurse for fields that are themselves structs. The cost is one extra pass per decode; for Cedar JSON payloads this is negligible.Migrate to json/v2
Unfortunately, Go's json v2 package, which enforces case sensitivity (amongst other niceties) still hasn't stabilized. However, we could wait to fix this bug until such time as it has.
Backwards compatibility
This is a tightening change: inputs that previously parsed or passed schema validation will now error in conformance with the Rust implementation. There is no desire to preserve backwards compatibility.