VYPR
Critical severity9.8NVD Advisory· Published Mar 27, 2026· Updated Apr 14, 2026

CVE-2026-33729

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.

PackageAffected versionsPatched versions
github.com/openfga/openfgaGo
< 1.13.11.13.1

Affected products

1
  • cpe:2.3:a:openfga:openfga:*:*:*:*:*:*:*:*
    Range: <1.13.1

Patches

1
049b50ccd2cc

Merge commit from fork

https://github.com/openfga/openfgaJustin CohenMar 23, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.