OpenFGA Authorization Bypass
Description
OpenFGA is a high-performance and flexible authorization/permission engine built for developers and inspired by Google Zanzibar. OpenFGA < v1.8.4 (Helm chart < openfga-0.2.22, docker < v.1.8.4) are vulnerable to authorization bypass when certain Check and ListObject calls are executed. Users on OpenFGA v1.8.4 or previous, specifically under the following conditions are affected by this authorization bypass vulnerability: 1. Calling Check API or ListObjects with a model that has a relation directly assignable to both public access AND userset with the same type. 2. A type bound public access tuple is assigned to an object. 3. userset tuple is not assigned to the same object. and 4. Check request's user field is a userset that has the same type as the type bound public access tuple's user type. Users are advised to upgrade to v1.8.5 which is backwards compatible. There are no known workarounds for this vulnerability.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/openfga/openfgaGo | < 1.8.5 | 1.8.5 |
Affected products
1Patches
18 files changed · +411 −2
CHANGELOG.md+1 −0 modified@@ -27,6 +27,7 @@ Try to keep listed changes to a concise bulleted list of simple explanations of - Fixed incorrect invalidation by cache controller on cache iterator. [#2190](https://github.com/openfga/openfga/pull/2190), [#2216](https://github.com/openfga/openfga/pull/2216) - Fixed incorrect types in configuration JSON schema [#2217](https://github.com/openfga/openfga/pull/2217), [#2228](https://github.com/openfga/openfga/pull/2228). - Fixed `BatchCheck` API to validate presence of the `tuple_key` property of a `BatchCheckItem` [#2242](https://github.com/openfga/openfga/issues/2242) +- Fixed incorrect check and list objects evaluation when model has a relation directly assignable to both public access AND userset with the same type and type bound public access tuple is assigned to the object. ## [1.8.4] - 2025-01-13 [Full changelog](https://github.com/openfga/openfga/compare/v1.8.3...v1.8.4)
internal/graph/check.go+5 −0 modified@@ -1111,6 +1111,11 @@ func shouldCheckPublicAssignable(ctx context.Context, reqTupleKey *openfgav1.Tup objectType := tuple.GetType(reqTupleKey.GetObject()) relation := reqTupleKey.GetRelation() + // if the user tuple is userset, by definition it cannot be a wildcard + if tuple.IsObjectRelation(reqTupleKey.GetUser()) { + return false + } + isPubliclyAssignable, _ := typesys.IsPubliclyAssignable( typesystem.DirectRelationReference(objectType, relation), // target tuple.GetType(reqTupleKey.GetUser()),
internal/graph/check_test.go+48 −0 modified@@ -4274,6 +4274,54 @@ func TestShouldCheckPubliclyAssigned(t *testing.T) { reqTupleKey: tuple.NewTupleKey("group:1", "other_member", "group:2#member"), expected: false, }, + { + name: "mixed_public_userset_tuple_user", + model: parser.MustTransformDSLToProto(` + model + schema 1.1 + type user + type group + relations + define member: [user, user:*] + type folder + relations + define viewer: [group, group:*, group#member] + `), + reqTupleKey: tuple.NewTupleKey("folder:1", "viewer", "group:1"), + expected: true, + }, + { + name: "mixed_public_userset_tuple_wilduser", + model: parser.MustTransformDSLToProto(` + model + schema 1.1 + type user + type group + relations + define member: [user, user:*] + type folder + relations + define viewer: [group, group:*, group#member] + `), + reqTupleKey: tuple.NewTupleKey("folder:1", "viewer", "group:*"), + expected: true, + }, + { + name: "mixed_public_userset_tuple_userset", + model: parser.MustTransformDSLToProto(` + model + schema 1.1 + type user + type group + relations + define member: [user, user:*] + type folder + relations + define viewer: [group, group:*, group#member] + `), + reqTupleKey: tuple.NewTupleKey("folder:1", "viewer", "group:1#member"), + expected: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {
pkg/server/commands/reverseexpand/reverse_expand.go+14 −1 modified@@ -423,6 +423,19 @@ func (c *ReverseExpandQuery) reverseExpandDirect( return err } +func (c *ReverseExpandQuery) shouldCheckPublicAssignable(targetReference *openfgav1.RelationReference, userRef IsUserRef) (bool, error) { + _, userIsUserset := userRef.(*UserRefObjectRelation) + if userIsUserset { + // if the user is an userset, by definition it is not public assignable + return false, nil + } + publiclyAssignable, err := c.typesystem.IsPubliclyAssignable(targetReference, userRef.GetObjectType()) + if err != nil { + return false, err + } + return publiclyAssignable, nil +} + func (c *ReverseExpandQuery) readTuplesAndExecute( ctx context.Context, req *ReverseExpandRequest, @@ -445,7 +458,7 @@ func (c *ReverseExpandQuery) readTuplesAndExecute( relationFilter = req.edge.TargetReference.GetRelation() targetUserObjectType := req.User.GetObjectType() - publiclyAssignable, err := c.typesystem.IsPubliclyAssignable(req.edge.TargetReference, targetUserObjectType) + publiclyAssignable, err := c.shouldCheckPublicAssignable(req.edge.TargetReference, req.User) if err != nil { return err }
pkg/server/commands/reverseexpand/reverse_expand_test.go+91 −0 modified@@ -820,3 +820,94 @@ func TestReverseExpandHonorsConsistency(t *testing.T) { } } } + +func TestShouldCheckPublicAssignable(t *testing.T) { + tests := []struct { + name string + model string + targetReference *openfgav1.RelationReference + userRef IsUserRef + expectedResult bool + expectedError bool + }{ + { + name: "model_non_public", + model: ` + model + schema 1.1 + type user + type group + relations + define member: [user] + type document + relations + define viewer: [user, group#member] +`, + targetReference: typesystem.DirectRelationReference("document", "viewer"), + userRef: &UserRefObject{ + Object: &openfgav1.Object{Type: "user", Id: "bob"}, + }, + expectedResult: false, + }, + { + name: "model_public", + model: ` + model + schema 1.1 + type user + type group + relations + define member: [user] + type document + relations + define viewer: [user, user:*, group#member] +`, + targetReference: typesystem.DirectRelationReference("document", "viewer"), + userRef: &UserRefObject{ + Object: &openfgav1.Object{Type: "user", Id: "bob"}, + }, + expectedResult: true, + }, + { + name: "model_public_but_request_userset", + model: ` + model + schema 1.1 + type user + type group + relations + define member: [user] + type document + relations + define viewer: [user, user:*, group#member] +`, + targetReference: typesystem.DirectRelationReference("document", "viewer"), + userRef: &UserRefObjectRelation{ + ObjectRelation: &openfgav1.ObjectRelation{ + Object: "group", + Relation: "member", + }, + }, + expectedResult: false, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + mockController := gomock.NewController(t) + defer mockController.Finish() + + typeSystem, err := typesystem.New(testutils.MustTransformDSLToProtoWithID(test.model)) + require.NoError(t, err) + + mockDatastore := mocks.NewMockOpenFGADatastore(mockController) + dut := NewReverseExpandQuery(mockDatastore, typeSystem) + publiclyAssignable, err := dut.shouldCheckPublicAssignable(test.targetReference, test.userRef) + if test.expectedError { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, test.expectedResult, publiclyAssignable) + } + }) + } +}
tests/check/check.go+4 −1 modified@@ -1146,6 +1146,8 @@ type usersets-user define ttu_and_direct_userset: [ttus#and_comp_from_direct_parent] define tuple_cycle2: [ttus#tuple_cycle2] define tuple_cycle3: [directs-user#compute_tuple_cycle3] + define userset_mix_public: [directs-user#direct, directs-user:*, user, user:*] + define or_userset_mix_public: [user, user:*] or userset_mix_public type ttus relations define direct_parent: [directs-user] @@ -1190,7 +1192,8 @@ type complexity3 define compute_userset_ttu_userset: userset_ttu_userset define or_compute_complex3: compute_ttu_userset_ttu or compute_userset_ttu_userset define and_nested_complex3: [ttus#and_ttu] and compute_ttu_userset_ttu - define cycle_nested: [ttus#tuple_cycle3] + define cycle_nested: [ttus#tuple_cycle3] + define or_userset_mix_public_complex3: or_userset_mix_public from userset_parent type complexity4 relations define userset_ttu_userset_ttu: [complexity3#ttu_userset_ttu]
tests/check/check_userset.go+153 −0 modified@@ -1462,4 +1462,157 @@ var usersetCompleteTestingModelTest = []*stage{ }, }, }, + { + Name: "userset_mix_public", + Tuples: []*openfgav1.TupleKey{ + {Object: "usersets-user:userset_mix_public_1", Relation: "userset_mix_public", User: "directs-user:userset_mix_public_1#direct"}, + {Object: "directs-user:userset_mix_public_1", Relation: "direct", User: "user:userset_mix_public_1"}, + + {Object: "usersets-user:userset_mix_public_user_public", Relation: "userset_mix_public", User: "user:*"}, + + {Object: "usersets-user:userset_mix_public_user_specific", Relation: "userset_mix_public", User: "user:specific"}, + {Object: "usersets-user:userset_mix_directs_user_public", Relation: "userset_mix_public", User: "directs-user:*"}, + }, + CheckAssertions: []*checktest.Assertion{ + { + Name: "valid_userset_assignment", + Tuple: &openfgav1.TupleKey{Object: "usersets-user:userset_mix_public_1", Relation: "userset_mix_public", User: "directs-user:userset_mix_public_1#direct"}, + Expectation: true, + }, + { + Name: "valid_user", + Tuple: &openfgav1.TupleKey{Object: "usersets-user:userset_mix_public_1", Relation: "userset_mix_public", User: "user:userset_mix_public_1"}, + Expectation: true, + }, + { + Name: "invalid_userset_assignment", + Tuple: &openfgav1.TupleKey{Object: "usersets-user:userset_mix_public_1", Relation: "userset_mix_public", User: "directs-user:userset_mix_public_invalid#direct"}, + Expectation: false, + }, + { + Name: "invalid_user", + Tuple: &openfgav1.TupleKey{Object: "usersets-user:userset_mix_public_1", Relation: "userset_mix_public", User: "user:userset_mix_public_invalid"}, + Expectation: false, + }, + { + Name: "user_public", + Tuple: &openfgav1.TupleKey{Object: "usersets-user:userset_mix_public_user_public", Relation: "userset_mix_public", User: "user:any"}, + Expectation: true, + }, + { + Name: "user_specific", + Tuple: &openfgav1.TupleKey{Object: "usersets-user:userset_mix_public_user_specific", Relation: "userset_mix_public", User: "user:specific"}, + Expectation: true, + }, + { + Name: "user_specific_other", + Tuple: &openfgav1.TupleKey{Object: "usersets-user:userset_mix_public_user_specific", Relation: "userset_mix_public", User: "user:other"}, + Expectation: false, + }, + { + Name: "direct_user_public", + Tuple: &openfgav1.TupleKey{Object: "usersets-user:userset_mix_directs_user_public", Relation: "userset_mix_public", User: "directs-user:any"}, + Expectation: true, + }, + { + Name: "direct_user_public_userset_1", + Tuple: &openfgav1.TupleKey{Object: "usersets-user:userset_mix_directs_user_public", Relation: "userset_mix_public", User: "directs-user:any#direct"}, + Expectation: false, + }, + { + Name: "direct_user_public_userset_2", + Tuple: &openfgav1.TupleKey{Object: "usersets-user:userset_mix_directs_user_public", Relation: "userset_mix_public", User: "directs-user:any#direct_wild"}, + Expectation: false, + }, + }, + }, + { + Name: "or_userset_mix_public", + Tuples: []*openfgav1.TupleKey{ + {Object: "usersets-user:or_userset_mix_public_1", Relation: "userset_mix_public", User: "directs-user:or_userset_mix_public_1#direct"}, + {Object: "directs-user:or_userset_mix_public_1", Relation: "direct", User: "user:or_userset_mix_public_1"}, + + {Object: "usersets-user:or_userset_mix_public_user_public", Relation: "userset_mix_public", User: "user:*"}, + + {Object: "usersets-user:or_userset_mix_public_user_specific", Relation: "userset_mix_public", User: "user:or_specific"}, + + {Object: "usersets-user:or_userset_mix_public_2", Relation: "or_userset_mix_public", User: "user:*"}, + + {Object: "usersets-user:or_userset_mix_public_3", Relation: "or_userset_mix_public", User: "user:or_userset_mix_public_3"}, + + {Object: "usersets-user:or_userset_mix_directs_user_public", Relation: "userset_mix_public", User: "directs-user:*"}, + }, + CheckAssertions: []*checktest.Assertion{ + { + Name: "valid_userset_assignment", + Tuple: &openfgav1.TupleKey{Object: "usersets-user:or_userset_mix_public_1", Relation: "or_userset_mix_public", User: "directs-user:or_userset_mix_public_1#direct"}, + Expectation: true, + }, + { + Name: "valid_user", + Tuple: &openfgav1.TupleKey{Object: "usersets-user:or_userset_mix_public_1", Relation: "or_userset_mix_public", User: "user:or_userset_mix_public_1"}, + Expectation: true, + }, + { + Name: "invalid_userset_assignment", + Tuple: &openfgav1.TupleKey{Object: "usersets-user:or_userset_mix_public_1", Relation: "or_userset_mix_public", User: "directs-user:or_userset_mix_public_invalid#direct"}, + Expectation: false, + }, + { + Name: "invalid_user", + Tuple: &openfgav1.TupleKey{Object: "usersets-user:or_userset_mix_public_1", Relation: "or_userset_mix_public", User: "user:or_userset_mix_public_invalid"}, + Expectation: false, + }, + { + Name: "user_public", + Tuple: &openfgav1.TupleKey{Object: "usersets-user:or_userset_mix_public_user_public", Relation: "or_userset_mix_public", User: "user:or_any"}, + Expectation: true, + }, + { + Name: "user_specific", + Tuple: &openfgav1.TupleKey{Object: "usersets-user:or_userset_mix_public_user_specific", Relation: "or_userset_mix_public", User: "user:or_specific"}, + Expectation: true, + }, + { + Name: "user_specific_other", + Tuple: &openfgav1.TupleKey{Object: "usersets-user:or_userset_mix_public_user_specific", Relation: "or_userset_mix_public", User: "user:or_other"}, + Expectation: false, + }, + { + Name: "public_user_direct_assign", + Tuple: &openfgav1.TupleKey{Object: "usersets-user:or_userset_mix_public_2", Relation: "or_userset_mix_public", User: "user:any"}, + Expectation: true, + }, + { + Name: "specific_user_direct_assign", + Tuple: &openfgav1.TupleKey{Object: "usersets-user:or_userset_mix_public_3", Relation: "or_userset_mix_public", User: "user:or_userset_mix_public_3"}, + Expectation: true, + }, + { + Name: "user_direct_assign_invalid_user", + Tuple: &openfgav1.TupleKey{Object: "usersets-user:or_userset_mix_public_3", Relation: "or_userset_mix_public", User: "user:or_userset_mix_public_3_invalid"}, + Expectation: false, + }, + { + Name: "user_direct_assign_invalid_object", + Tuple: &openfgav1.TupleKey{Object: "usersets-user:or_userset_mix_public_3_invalid", Relation: "or_userset_mix_public", User: "user:or_userset_mix_public_3"}, + Expectation: false, + }, + { + Name: "direct_user_public", + Tuple: &openfgav1.TupleKey{Object: "usersets-user:or_userset_mix_directs_user_public", Relation: "or_userset_mix_public", User: "directs-user:any"}, + Expectation: true, + }, + { + Name: "direct_user_public_userset_1", + Tuple: &openfgav1.TupleKey{Object: "usersets-user:or_userset_mix_directs_user_public", Relation: "or_userset_mix_public", User: "directs-user:any#direct"}, + Expectation: false, + }, + { + Name: "direct_user_public_userset_2", + Tuple: &openfgav1.TupleKey{Object: "usersets-user:or_userset_mix_directs_user_public", Relation: "or_userset_mix_public", User: "directs-user:any#direct_wild"}, + Expectation: false, + }, + }, + }, }
tests/check/complexity_three.go+95 −0 modified@@ -824,4 +824,99 @@ var complexityThreeTestingModelTest = []*stage{ }, }, }, + { + Name: "complex3_or_userset_mix_public", + Tuples: []*openfgav1.TupleKey{ + {Object: "usersets-user:complex3_or_userset_mix_public_1", Relation: "userset_mix_public", User: "directs-user:complex3_or_userset_mix_public_1#direct"}, + {Object: "directs-user:complex3_or_userset_mix_public_1", Relation: "direct", User: "user:complex3_or_userset_mix_public_1"}, + {Object: "complexity3:complex3_or_userset_mix_public_1", Relation: "userset_parent", User: "usersets-user:complex3_or_userset_mix_public_1"}, + + {Object: "usersets-user:complex3_or_userset_mix_public_user_public", Relation: "userset_mix_public", User: "user:*"}, + {Object: "complexity3:complex3_or_userset_mix_public_user_public", Relation: "userset_parent", User: "usersets-user:complex3_or_userset_mix_public_user_public"}, + + {Object: "usersets-user:complex3_or_userset_mix_public_user_specific", Relation: "userset_mix_public", User: "user:or_specific"}, + {Object: "complexity3:complex3_or_userset_mix_public_user_specific", Relation: "userset_parent", User: "usersets-user:complex3_or_userset_mix_public_user_specific"}, + + {Object: "usersets-user:complex3_or_userset_mix_public_2", Relation: "or_userset_mix_public", User: "user:*"}, + {Object: "complexity3:complex3_or_userset_mix_public_2", Relation: "userset_parent", User: "usersets-user:complex3_or_userset_mix_public_2"}, + + {Object: "usersets-user:complex3_or_userset_mix_public_3", Relation: "or_userset_mix_public", User: "user:complex3_or_userset_mix_public_3"}, + {Object: "complexity3:complex3_or_userset_mix_public_3", Relation: "userset_parent", User: "usersets-user:complex3_or_userset_mix_public_3"}, + + {Object: "usersets-user:complex3_or_userset_mix_directs_user_public", Relation: "userset_mix_public", User: "directs-user:*"}, + {Object: "complexity3:complex3_or_userset_mix_directs_user_public", Relation: "userset_parent", User: "usersets-user:complex3_or_userset_mix_directs_user_public"}, + }, + CheckAssertions: []*checktest.Assertion{ + { + Name: "valid_userset_assignment", + Tuple: &openfgav1.TupleKey{Object: "complexity3:complex3_or_userset_mix_public_1", Relation: "or_userset_mix_public_complex3", User: "directs-user:complex3_or_userset_mix_public_1#direct"}, + Expectation: true, + }, + { + Name: "valid_user", + Tuple: &openfgav1.TupleKey{Object: "complexity3:complex3_or_userset_mix_public_1", Relation: "or_userset_mix_public_complex3", User: "user:complex3_or_userset_mix_public_1"}, + Expectation: true, + }, + { + Name: "invalid_userset_assignment", + Tuple: &openfgav1.TupleKey{Object: "complexity3:complex3_or_userset_mix_public_1", Relation: "or_userset_mix_public_complex3", User: "directs-user:complex3_or_userset_mix_public_invalid#direct"}, + Expectation: false, + }, + { + Name: "invalid_user", + Tuple: &openfgav1.TupleKey{Object: "complexity3:complex3_or_userset_mix_public_1", Relation: "or_userset_mix_public_complex3", User: "user:complex3_or_userset_mix_public_invalid"}, + Expectation: false, + }, + { + Name: "user_public", + Tuple: &openfgav1.TupleKey{Object: "complexity3:complex3_or_userset_mix_public_user_public", Relation: "or_userset_mix_public_complex3", User: "user:or_any"}, + Expectation: true, + }, + { + Name: "user_specific", + Tuple: &openfgav1.TupleKey{Object: "complexity3:complex3_or_userset_mix_public_user_specific", Relation: "or_userset_mix_public_complex3", User: "user:or_specific"}, + Expectation: true, + }, + { + Name: "user_specific_other", + Tuple: &openfgav1.TupleKey{Object: "complexity3:complex3_or_userset_mix_public_user_specific", Relation: "or_userset_mix_public_complex3", User: "user:or_other"}, + Expectation: false, + }, + { + Name: "public_user_direct_assign", + Tuple: &openfgav1.TupleKey{Object: "complexity3:complex3_or_userset_mix_public_2", Relation: "or_userset_mix_public_complex3", User: "user:any"}, + Expectation: true, + }, + { + Name: "specific_user_direct_assign", + Tuple: &openfgav1.TupleKey{Object: "complexity3:complex3_or_userset_mix_public_3", Relation: "or_userset_mix_public_complex3", User: "user:complex3_or_userset_mix_public_3"}, + Expectation: true, + }, + { + Name: "user_direct_assign_invalid_user", + Tuple: &openfgav1.TupleKey{Object: "complexity3:complex3_or_userset_mix_public_3", Relation: "or_userset_mix_public_complex3", User: "user:complex3_or_userset_mix_public_3_invalid"}, + Expectation: false, + }, + { + Name: "user_direct_assign_invalid_object", + Tuple: &openfgav1.TupleKey{Object: "complexity3:complex3_or_userset_mix_public_3_invalid", Relation: "or_userset_mix_public_complex3", User: "user:complex3_or_userset_mix_public_3"}, + Expectation: false, + }, + { + Name: "direct_user_public", + Tuple: &openfgav1.TupleKey{Object: "complexity3:complex3_or_userset_mix_directs_user_public", Relation: "or_userset_mix_public_complex3", User: "directs-user:any"}, + Expectation: true, + }, + { + Name: "direct_user_public_userset_1", + Tuple: &openfgav1.TupleKey{Object: "complexity3:complex3_or_userset_mix_directs_user_public", Relation: "or_userset_mix_public_complex3", User: "directs-user:any#direct"}, + Expectation: false, + }, + { + Name: "direct_user_public_userset_2", + Tuple: &openfgav1.TupleKey{Object: "complexity3:complex3_or_userset_mix_directs_user_public", Relation: "or_userset_mix_public_complex3", User: "directs-user:any#direct_wild"}, + Expectation: false, + }, + }, + }, }
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
4- github.com/advisories/GHSA-g4v5-6f5p-m38jghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-25196ghsaADVISORY
- github.com/openfga/openfga/commit/0aee4f47e0c642de78831ceb27bb62b116f49588ghsax_refsource_MISCWEB
- github.com/openfga/openfga/security/advisories/GHSA-g4v5-6f5p-m38jghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.