CVE-2026-33729
Description
OpenFGA is a high-performance and flexible authorization/permission engine built for developers and inspired by Google Zanzibar. In versions prior to 1.13.1, under specific conditions, models using conditions with caching enabled can result in two different check requests producing the same cache key. This can result in OpenFGA reusing an earlier cached result for a different request. Users are affected if the model has relations which rely on condition evaluation andncaching is enabled. OpenFGA v1.13.1 contains a patch.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/openfga/openfgaGo | < 1.13.1 | 1.13.1 |
Affected products
1Patches
1049b50ccd2ccMerge commit from fork
3 files changed · +94 −6
CHANGELOG.md+3 −0 modified@@ -8,6 +8,9 @@ Try to keep listed changes to a concise bulleted list of simple explanations of ## [Unreleased] +### Fixed +- Fixed a bug in cache key construction for Check requests using conditions. + ## [1.13.0] - 2026-03-23 ### Added - Add AuthZen 1.0 experimental support. [#2875](https://github.com/openfga/openfga/pull/2875)
pkg/storage/cache.go+21 −1 modified@@ -10,6 +10,7 @@ import ( "strconv" "sync" "time" + "unicode" "github.com/Yiling-J/theine-go" "github.com/prometheus/client_golang/prometheus" @@ -270,7 +271,20 @@ func writeValue(w io.StringWriter, v *structpb.Value) (err error) { case *structpb.Value_NullValue: _, err = w.WriteString("null") case *structpb.Value_StringValue: - _, err = w.WriteString(val.StringValue) + for _, c := range val.StringValue { + // Strip out any control characters so strings can't be manipulated + if unicode.IsControl(c) { + _, err = w.WriteString("?") + if err != nil { + return + } + continue + } + + if _, err = w.WriteString(string(c)); err != nil { + return + } + } case *structpb.Value_NumberValue: _, err = w.WriteString(strconv.FormatFloat(val.NumberValue, 'f', -1, 64)) // -1 precision ensures we represent the 64-bit value with the maximum precision needed to represent it, see strconv#FormatFloat for more info. case *structpb.Value_ListValue: @@ -321,6 +335,12 @@ func writeStruct(w io.StringWriter, s *structpb.Struct) (err error) { keys := keys(fields) sort.Strings(keys) + // encode the length of the keys to ensure the correct number of keys are reflected + // e.g. "a": "x,'b:'y" + if _, err = w.WriteString(strconv.Itoa(len(keys))); err != nil { + return + } + for _, key := range keys { if _, err = w.WriteString(fmt.Sprintf("'%s:'", key)); err != nil { return
pkg/storage/cache_test.go+70 −5 modified@@ -100,7 +100,7 @@ func TestWriteValue(t *testing.T) { })), }, }), - output: "A,null,true,1111111111,'key:'value,", + output: "A,null,true,1111111111,1'key:'value,", }, "list_write_value_error": { writer: &ErrorStringWriter{}, @@ -163,7 +163,16 @@ func TestWriteStruct(t *testing.T) { "keyA": "valueA", "keyB": "valueB", }), - output: "'keyA:'valueA,'keyB:'valueB,", + output: "2'keyA:'valueA,'keyB:'valueB,", + }, + "incorrect_value": { + writer: &validWriter, + value: MustNewStruct(map[string]any{ + // This value is crafted to appear as if it were 2 keys + "a": "x,'b:'y", + }), + // but our cache key should identify it correctly as 1 key + output: "1'a:'x,'b:'y,", }, "fields_write_key_error": { writer: &ErrorStringWriter{}, @@ -292,7 +301,7 @@ func TestWriteTuples(t *testing.T) { }), ), }, - output: "/document:A#relationA with A 'key:'value,@user:A,document:A#relationA with B 'key:'value,@user:A", + output: "/document:A#relationA with A 1'key:'value,@user:A,document:A#relationA with B 1'key:'value,@user:A", }, "with_condition_write_with_error": { writer: &ErrorStringWriter{ @@ -769,6 +778,62 @@ func TestCheckCacheKeyConditionContextOrderAgnostic(t *testing.T) { require.Equal(t, key1, key2) } +func TestCheckCacheKeySanitizesUnicodeControlCharacters(t *testing.T) { + storeID := ulid.Make().String() + modelID := ulid.Make().String() + + struct1, err := structpb.NewStruct(map[string]interface{}{ + "a": "x", + }) + require.NoError(t, err) + + // The unicode chars below are backspaces. + // Without sanitization, the cache key for struct1 and struct2 are the same. + struct2, err := structpb.NewStruct(map[string]interface{}{ + "a": "y", + "b": "\u0008\u0008\u0008\u0008\u0008\u0008x", + }) + + require.NoError(t, err) + + jonContextOne := tuple.NewTupleKeyWithCondition( + "document:2", + "admin", + "user:jon", + "some_condition", + struct1, + ) + + jonContextTwo := tuple.NewTupleKeyWithCondition( + "document:2", + "admin", + "user:jon", + "some_condition", + struct2, + ) + + tupleKey := tuple.NewTupleKey("document:x", "viewer", "user:jon") + + key1 := MustGetCheckCacheKey(&CheckCacheKeyParams{ + StoreID: storeID, + AuthorizationModelID: modelID, + TupleKey: tupleKey, + ContextualTuples: []*openfgav1.TupleKey{jonContextOne}, + }) + + key2 := MustGetCheckCacheKey(&CheckCacheKeyParams{ + StoreID: storeID, + AuthorizationModelID: modelID, + TupleKey: tupleKey, + ContextualTuples: []*openfgav1.TupleKey{jonContextTwo}, + }) + + require.NotEqual(t, key1, key2) + + // One '?' for each backspace character + require.Contains(t, key2, "??????") +} + func TestCheckCacheKeyContextualTuplesConditionsOrderDoesNotMatter(t *testing.T) { storeID := ulid.Make().String() modelID := ulid.Make().String() @@ -886,7 +951,7 @@ func TestWriteInvariantCheckCacheKey(t *testing.T) { }, Context: contextStruct, }, - output: "fake_model_id/document:1#viewer with condition_name 'key1:'true,@user:anne'key1:'true,", + output: "fake_model_id/document:1#viewer with condition_name 1'key1:'true,@user:anne1'key1:'true,", error: false, }, "writer_error": { @@ -944,7 +1009,7 @@ func TestWriteCheckCacheKey(t *testing.T) { }, Context: contextStruct, }, - output: "document:1#can_view@user:annefake_model_id/document:1#viewer with condition_name 'key1:'true,@user:anne'key1:'true,", + output: "document:1#can_view@user:annefake_model_id/document:1#viewer with condition_name 1'key1:'true,@user:anne1'key1:'true,", }, } for name, test := range cases {
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/openfga/openfga/commit/049b50ccd2cc7e163bd897f3d17a7b859ad146f8nvdPatchWEB
- github.com/advisories/GHSA-h6c8-cww8-35hfghsaADVISORY
- github.com/openfga/openfga/security/advisories/GHSA-h6c8-cww8-35hfnvdVendor AdvisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-33729ghsaADVISORY
- github.com/openfga/openfga/releases/tag/v1.13.1nvdRelease NotesWEB
News mentions
0No linked articles in our index yet.