VYPR
Moderate severityNVD Advisory· Published May 22, 2025· Updated May 23, 2025

OpenFGA Authorization Bypass

CVE-2025-48371

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.

PackageAffected versionsPatched versions
github.com/openfga/openfgaGo
>= 1.8.0, < 1.8.131.8.13

Affected products

1

Patches

1
e5960d4eba92

Merge commit from fork

https://github.com/openfga/openfgaAdrian TamMay 22, 2025via ghsa
4 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

News mentions

0

No linked articles in our index yet.