OpenFGA Authorization Bypass
Description
OpenFGA is an authorization/permission engine. OpenFGA versions 1.8.0 through 1.8.12 (corresponding to Helm chart openfga-0.2.16 through openfga-0.2.30 and docker 1.8.0 through 1.8.12) are vulnerable to authorization bypass when certain Check and ListObject calls are executed. Users are affected under four specific conditions: First, calling Check API or ListObjects with an authorization model that has a relationship directly assignable by both type bound public access and userset; second, there are check or list object queries with contextual tuples for the relationship that can be directly assignable by both type bound public access and userset; third, those contextual tuples’s user field is an userset; and finally, type bound public access tuples are not assigned to the relationship. Users should upgrade to version 1.8.13 to receive a patch. The upgrade is backwards compatible.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/openfga/openfgaGo | >= 1.8.0, < 1.8.13 | 1.8.13 |
Affected products
1Patches
14 files changed · +309 −6
assets/tests/consolidated_1_1_tests.yaml+23 −1 modified@@ -10385,4 +10385,26 @@ tests: user: user:maria relation: rel1 object: document:x - expectation: true \ No newline at end of file + expectation: true + - name: combined_public_wildcard_userset + stages: + - model: | + model + schema 1.1 + type user + type role + relations + define assignee: [user] + type deployment + relations + define can_access: [user:*, role#assignee] + tuples: + - user: role:superadmin#assignee + relation: can_access + object: deployment:1 + checkAssertions: + - tuple: + user: user:jdoe + relation: can_access + object: deployment:1 + expectation: false \ No newline at end of file
CHANGELOG.md+5 −1 modified@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Try to keep listed changes to a concise bulleted list of simple explanations of changes. Aim for the amount of information needed so that readers can understand where they would look in the codebase to investigate the changes' implementation, or where they would look in the documentation to understand how to make use of the change in practice - better yet, link directly to the docs and provide detailed information there. Only elaborate if doing so is required to avoid breaking changes or experimental features from ruining someone's day. ## [Unreleased] + +## [1.8.13] - 2025-05-22 ### Added - New `DatastoreThrottle` configuration for Check, ListObjects, ListUsers. [#2452](https://github.com/openfga/openfga/pull/2452) - Added pkg `migrate` to expose `.RunMigrations()` for programmatic use. [#2422](https://github.com/openfga/openfga/pull/2422) @@ -16,6 +18,7 @@ Try to keep listed changes to a concise bulleted list of simple explanations of ### Fixed - Ensure `fanin.Stop` and `fanin.Drain` are called for all clients which may create blocking goroutines. [#2441](https://github.com/openfga/openfga/pull/2441) - Prevent throttled Go routines from "leaking" when a request context has been canceled or deadline exceeded. [#2450](https://github.com/openfga/openfga/pull/2450) +- Filter context tuples based on type restrictions for ReadUsersetTuples. [CVE-2025-48371](https://github.com/openfga/openfga/security/advisories/GHSA-c72g-53hw-82q7) ## [1.8.12] - 2025-05-12 [Full changelog](https://github.com/openfga/openfga/compare/v1.8.11...v1.8.12) @@ -1298,7 +1301,8 @@ Re-release of `v0.3.5` because the go module proxy cached a prior commit of the - Memory storage adapter implementation - Early support for preshared key or OIDC authentication methods -[Unreleased]: https://github.com/openfga/openfga/compare/v1.8.12...HEAD +[Unreleased]: https://github.com/openfga/openfga/compare/v1.8.13...HEAD +[1.8.13]: https://github.com/openfga/openfga/compare/v1.8.12...v1.8.13 [1.8.12]: https://github.com/openfga/openfga/compare/v1.8.11...v1.8.12 [1.8.11]: https://github.com/openfga/openfga/compare/v1.8.10...v1.8.11 [1.8.10]: https://github.com/openfga/openfga/compare/v1.8.9...v1.8.10
pkg/storage/storagewrappers/combinedtuplereader.go+27 −1 modified@@ -104,6 +104,32 @@ func (c *CombinedTupleReader) ReadUserTuple( return c.RelationshipTupleReader.ReadUserTuple(ctx, store, tk, options) } +func tupleMatchesAllowedUserTypeRestrictions(t *openfgav1.Tuple, + allowedUserTypeRestrictions []*openfgav1.RelationReference) bool { + tupleUser := t.GetKey().GetUser() + if tuple.GetUserTypeFromUser(tupleUser) != tuple.UserSet { + return false + } + // We expect there is always allowedUserTypeRestrictions. If none is specified, + // the request itself is unexpected and the safe thing is not return the + // contextual tuples. + for _, allowedUserType := range allowedUserTypeRestrictions { + if _, ok := allowedUserType.GetRelationOrWildcard().(*openfgav1.RelationReference_Wildcard); ok { + if tuple.IsTypedWildcard(tupleUser) && tuple.GetType(tupleUser) == allowedUserType.GetType() { + return true + } + } + if _, ok := allowedUserType.GetRelationOrWildcard().(*openfgav1.RelationReference_Relation); ok { + if tuple.IsObjectRelation(tupleUser) && + tuple.GetType(tupleUser) == allowedUserType.GetType() && + tuple.GetRelation(tupleUser) == allowedUserType.GetRelation() { + return true + } + } + } + return false +} + // ReadUsersetTuples see [storage.RelationshipTupleReader.ReadUsersetTuples]. func (c *CombinedTupleReader) ReadUsersetTuples( ctx context.Context, @@ -114,7 +140,7 @@ func (c *CombinedTupleReader) ReadUsersetTuples( var usersetTuples []*openfgav1.Tuple for _, t := range filterTuples(c.contextualTuplesOrderedByObjectID, filter.Object, filter.Relation, []string{}) { - if tuple.GetUserTypeFromUser(t.GetKey().GetUser()) == tuple.UserSet { + if tupleMatchesAllowedUserTypeRestrictions(t, filter.AllowedUserTypeRestrictions) { usersetTuples = append(usersetTuples, t) } }
pkg/storage/storagewrappers/combinedtuplereader_test.go+254 −3 modified@@ -17,6 +17,7 @@ import ( "github.com/openfga/openfga/internal/mocks" "github.com/openfga/openfga/pkg/storage" "github.com/openfga/openfga/pkg/tuple" + "github.com/openfga/openfga/pkg/typesystem" ) var ( @@ -31,6 +32,7 @@ var ( "group:3#member@user:11", // userset tuples "folder:backlog#viewer@group:1#member", + "folder:backlog#viewer@group:*", } { result[key] = &openfgav1.Tuple{Key: tuple.MustParseTupleString(key)} } @@ -685,9 +687,15 @@ func Test_combinedTupleReader_ReadUsersetTuples(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - store: "1", - filter: storage.ReadUsersetTuplesFilter{}, + ctx: context.Background(), + store: "1", + filter: storage.ReadUsersetTuplesFilter{ + Object: "group", + Relation: "member", + AllowedUserTypeRestrictions: []*openfgav1.RelationReference{ + typesystem.WildcardRelationReference("group"), + }, + }, options: storage.ReadUsersetTuplesOptions{}, }, setup: func() { @@ -715,6 +723,9 @@ func Test_combinedTupleReader_ReadUsersetTuples(t *testing.T) { filter: storage.ReadUsersetTuplesFilter{ Object: "folder:backlog", Relation: "viewer", + AllowedUserTypeRestrictions: []*openfgav1.RelationReference{ + typesystem.DirectRelationReference("group", "member"), + }, }, options: storage.ReadUsersetTuplesOptions{}, }, @@ -744,6 +755,9 @@ func Test_combinedTupleReader_ReadUsersetTuples(t *testing.T) { filter: storage.ReadUsersetTuplesFilter{ Object: "folder:backlog", Relation: "viewer", + AllowedUserTypeRestrictions: []*openfgav1.RelationReference{ + typesystem.DirectRelationReference("group", "member"), + }, }, options: storage.ReadUsersetTuplesOptions{}, }, @@ -756,6 +770,243 @@ func Test_combinedTupleReader_ReadUsersetTuples(t *testing.T) { want: nil, wantErr: errors.New("test read error"), }, + { + name: "filter_wildcard_tuple_not_wildcard", + fields: fields{ + RelationshipTupleReader: mockRelationshipTupleReader, + contextualTuples: []*openfgav1.TupleKey{ + testTuples["folder:backlog#viewer@group:1#member"].GetKey(), + }, + }, + args: args{ + ctx: context.Background(), + store: "1", + filter: storage.ReadUsersetTuplesFilter{ + Object: "folder:backlog", + Relation: "viewer", + AllowedUserTypeRestrictions: []*openfgav1.RelationReference{ + typesystem.WildcardRelationReference("group"), + }, + }, + options: storage.ReadUsersetTuplesOptions{}, + }, + setup: func() { + mockRelationshipTupleReader. + EXPECT(). + ReadUsersetTuples(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(storage.NewStaticTupleIterator([]*openfgav1.Tuple{}), nil) + }, + want: []*openfgav1.Tuple{}, + wantErr: nil, + }, + { + name: "filter_wildcard_tuple_wrong_type", + fields: fields{ + RelationshipTupleReader: mockRelationshipTupleReader, + contextualTuples: []*openfgav1.TupleKey{ + testTuples["folder:backlog#viewer@group:*"].GetKey(), + }, + }, + args: args{ + ctx: context.Background(), + store: "1", + filter: storage.ReadUsersetTuplesFilter{ + Object: "folder:backlog", + Relation: "viewer", + AllowedUserTypeRestrictions: []*openfgav1.RelationReference{ + typesystem.WildcardRelationReference("foo"), + }, + }, + options: storage.ReadUsersetTuplesOptions{}, + }, + setup: func() { + mockRelationshipTupleReader. + EXPECT(). + ReadUsersetTuples(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(storage.NewStaticTupleIterator([]*openfgav1.Tuple{}), nil) + }, + want: []*openfgav1.Tuple{}, + wantErr: nil, + }, + { + name: "filter_wildcard_tuple_matches_filter", + fields: fields{ + RelationshipTupleReader: mockRelationshipTupleReader, + contextualTuples: []*openfgav1.TupleKey{ + testTuples["folder:backlog#viewer@group:*"].GetKey(), + }, + }, + args: args{ + ctx: context.Background(), + store: "1", + filter: storage.ReadUsersetTuplesFilter{ + Object: "folder:backlog", + Relation: "viewer", + AllowedUserTypeRestrictions: []*openfgav1.RelationReference{ + typesystem.WildcardRelationReference("group"), + }, + }, + options: storage.ReadUsersetTuplesOptions{}, + }, + setup: func() { + mockRelationshipTupleReader. + EXPECT(). + ReadUsersetTuples(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(storage.NewStaticTupleIterator([]*openfgav1.Tuple{}), nil) + }, + want: []*openfgav1.Tuple{ + testTuples["folder:backlog#viewer@group:*"], + }, + wantErr: nil, + }, + { + name: "filter_userset_tuple_not_userset", + fields: fields{ + RelationshipTupleReader: mockRelationshipTupleReader, + contextualTuples: []*openfgav1.TupleKey{ + testTuples["folder:backlog#viewer@group:*"].GetKey(), + }, + }, + args: args{ + ctx: context.Background(), + store: "1", + filter: storage.ReadUsersetTuplesFilter{ + Object: "folder:backlog", + Relation: "viewer", + AllowedUserTypeRestrictions: []*openfgav1.RelationReference{ + typesystem.DirectRelationReference("group", "member"), + }, + }, + options: storage.ReadUsersetTuplesOptions{}, + }, + setup: func() { + mockRelationshipTupleReader. + EXPECT(). + ReadUsersetTuples(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(storage.NewStaticTupleIterator([]*openfgav1.Tuple{}), nil) + }, + want: []*openfgav1.Tuple{}, + wantErr: nil, + }, + { + name: "filter_userset_tuple_not_match_type", + fields: fields{ + RelationshipTupleReader: mockRelationshipTupleReader, + contextualTuples: []*openfgav1.TupleKey{ + testTuples["folder:backlog#viewer@group:1#member"].GetKey(), + }, + }, + args: args{ + ctx: context.Background(), + store: "1", + filter: storage.ReadUsersetTuplesFilter{ + Object: "folder:backlog", + Relation: "viewer", + AllowedUserTypeRestrictions: []*openfgav1.RelationReference{ + typesystem.DirectRelationReference("other", "member"), + }, + }, + options: storage.ReadUsersetTuplesOptions{}, + }, + setup: func() { + mockRelationshipTupleReader. + EXPECT(). + ReadUsersetTuples(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(storage.NewStaticTupleIterator([]*openfgav1.Tuple{}), nil) + }, + want: []*openfgav1.Tuple{}, + wantErr: nil, + }, + { + name: "filter_userset_tuple_not_match_relation", + fields: fields{ + RelationshipTupleReader: mockRelationshipTupleReader, + contextualTuples: []*openfgav1.TupleKey{ + testTuples["folder:backlog#viewer@group:1#member"].GetKey(), + }, + }, + args: args{ + ctx: context.Background(), + store: "1", + filter: storage.ReadUsersetTuplesFilter{ + Object: "folder:backlog", + Relation: "viewer", + AllowedUserTypeRestrictions: []*openfgav1.RelationReference{ + typesystem.DirectRelationReference("group", "owner"), + }, + }, + options: storage.ReadUsersetTuplesOptions{}, + }, + setup: func() { + mockRelationshipTupleReader. + EXPECT(). + ReadUsersetTuples(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(storage.NewStaticTupleIterator([]*openfgav1.Tuple{}), nil) + }, + want: []*openfgav1.Tuple{}, + wantErr: nil, + }, + { + name: "filter_userset_tuple_matches", + fields: fields{ + RelationshipTupleReader: mockRelationshipTupleReader, + contextualTuples: []*openfgav1.TupleKey{ + testTuples["folder:backlog#viewer@group:1#member"].GetKey(), + }, + }, + args: args{ + ctx: context.Background(), + store: "1", + filter: storage.ReadUsersetTuplesFilter{ + Object: "folder:backlog", + Relation: "viewer", + AllowedUserTypeRestrictions: []*openfgav1.RelationReference{ + typesystem.DirectRelationReference("group", "member"), + typesystem.DirectRelationReference("group", "other"), + }, + }, + options: storage.ReadUsersetTuplesOptions{}, + }, + setup: func() { + mockRelationshipTupleReader. + EXPECT(). + ReadUsersetTuples(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(storage.NewStaticTupleIterator([]*openfgav1.Tuple{}), nil) + }, + want: []*openfgav1.Tuple{ + testTuples["folder:backlog#viewer@group:1#member"], + }, + wantErr: nil, + }, + { + name: "empty_restrictions", + fields: fields{ + RelationshipTupleReader: mockRelationshipTupleReader, + contextualTuples: []*openfgav1.TupleKey{ + testTuples["folder:backlog#viewer@group:1#member"].GetKey(), + }, + }, + args: args{ + ctx: context.Background(), + store: "1", + filter: storage.ReadUsersetTuplesFilter{ + Object: "folder:backlog", + Relation: "viewer", + // this should never happen. In real life, the safe thing to do is to + // ignore the tuple + AllowedUserTypeRestrictions: []*openfgav1.RelationReference{}, + }, + options: storage.ReadUsersetTuplesOptions{}, + }, + setup: func() { + mockRelationshipTupleReader. + EXPECT(). + ReadUsersetTuples(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(storage.NewStaticTupleIterator([]*openfgav1.Tuple{}), nil) + }, + want: []*openfgav1.Tuple{}, + wantErr: nil, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {
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/advisories/GHSA-c72g-53hw-82q7ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-48371ghsaADVISORY
- github.com/openfga/openfga/commit/e5960d4eba92b723de8ff3a5346a07f50c1379caghsax_refsource_MISCWEB
- github.com/openfga/openfga/security/advisories/GHSA-c72g-53hw-82q7ghsax_refsource_CONFIRMWEB
- pkg.go.dev/vuln/GO-2025-3707ghsaWEB
News mentions
0No linked articles in our index yet.