Moderate severityNVD Advisory· Published Oct 25, 2022· Updated Apr 23, 2025
OpenFGA Authorization Bypass
CVE-2022-39341
Description
OpenFGA is an authorization/permission engine. Versions prior to version 0.2.4 are vulnerable to authorization bypass under certain conditions. Users who have wildcard (*) defined on tupleset relations in their authorization model are vulnerable. Version 0.2.4 contains a patch for this issue.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/openfga/openfgaGo | < 0.2.4 | 0.2.4 |
Affected products
1Patches
1b466769cc100Merge pull request from GHSA-vj4m-83m8-xpw5
5 files changed · +196 −19
server/commands/check.go+22 −2 modified@@ -2,6 +2,7 @@ package commands import ( "context" + "fmt" "sync" "github.com/go-errors/errors" @@ -21,7 +22,7 @@ import ( "golang.org/x/sync/errgroup" ) -const AllUsers = "*" +const Wildcard = "*" // A CheckQuery can be used to Check if a User has a Relation to an Object // CheckQuery instances may be safely shared by multiple go-routines @@ -401,7 +402,9 @@ func (query *CheckQuery) resolveTupleToUserset(ctx context.Context, rc *resoluti if relation == "" { relation = rc.tk.GetRelation() } - findTK := &openfgapb.TupleKey{Object: rc.tk.GetObject(), Relation: relation} + + findTK := tupleUtils.NewTupleKey(rc.tk.GetObject(), relation, "") + tracer := rc.tracer.AppendTupleToUserset().AppendString(tupleUtils.ToObjectRelationString(findTK.GetObject(), relation)) iter, err := rc.read(ctx, query.datastore, findTK) if err != nil { @@ -429,6 +432,23 @@ func (query *CheckQuery) resolveTupleToUserset(ctx context.Context, rc *resoluti userObj, userRel := tupleUtils.SplitObjectRelation(tuple.GetUser()) + if userObj == Wildcard { + objectType, _ := tupleUtils.SplitObject(rc.tk.GetObject()) + + query.logger.WarnWithContext( + ctx, + fmt.Sprintf("unexpected wildcard evaluated on tupleset relation '%s'", relation), + zap.String("store_id", rc.store), + zap.String("authorization_model_id", rc.modelID), + zap.String("object_type", objectType), + ) + + return serverErrors.InvalidTuple( + fmt.Sprintf("unexpected wildcard evaluated on relation '%s#%s'", objectType, relation), + tupleUtils.NewTupleKey(rc.tk.GetObject(), relation, Wildcard), + ) + } + if !tupleUtils.IsValidObject(userObj) { continue // TupleToUserset tuplesets should be of the form 'objectType:id' or 'objectType:id#relation' but are not guaranteed to be because it is neither a user or userset }
server/commands/check_utils.go+1 −1 modified@@ -195,7 +195,7 @@ func (u *userSet) Get(value string) (resolutionTracer, bool) { var found bool var rt resolutionTracer if rt, found = u.u[value]; !found { - if rt, found = u.u[AllUsers]; !found { + if rt, found = u.u[Wildcard]; !found { return nil, false } }
server/commands/expand.go+47 −5 modified@@ -2,6 +2,7 @@ package commands import ( "context" + "fmt" "github.com/openfga/openfga/pkg/logger" tupleUtils "github.com/openfga/openfga/pkg/tuple" @@ -11,6 +12,7 @@ import ( "github.com/openfga/openfga/storage" openfgapb "go.buf.build/openfga/go/openfga/api/openfga/v1" "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" "golang.org/x/sync/errgroup" ) @@ -58,7 +60,13 @@ func (query *ExpandQuery) Execute(ctx context.Context, req *openfgapb.ExpandRequ }, nil } -func (query *ExpandQuery) resolveUserset(ctx context.Context, store, modelID string, userset *openfgapb.Userset, tk *openfgapb.TupleKey, metadata *utils.ResolutionMetadata) (*openfgapb.UsersetTree_Node, error) { +func (query *ExpandQuery) resolveUserset( + ctx context.Context, + store, modelID string, + userset *openfgapb.Userset, + tk *openfgapb.TupleKey, + metadata *utils.ResolutionMetadata, +) (*openfgapb.UsersetTree_Node, error) { ctx, span := query.tracer.Start(ctx, "resolveUserset") defer span.End() @@ -68,7 +76,7 @@ func (query *ExpandQuery) resolveUserset(ctx context.Context, store, modelID str case *openfgapb.Userset_ComputedUserset: return query.resolveComputedUserset(ctx, us.ComputedUserset, tk, metadata) case *openfgapb.Userset_TupleToUserset: - return query.resolveTupleToUserset(ctx, store, us.TupleToUserset, tk, metadata) + return query.resolveTupleToUserset(ctx, store, modelID, us.TupleToUserset, tk, metadata) case *openfgapb.Userset_Union: return query.resolveUnionUserset(ctx, store, modelID, us.Union, tk, metadata) case *openfgapb.Userset_Difference: @@ -152,14 +160,24 @@ func (query *ExpandQuery) resolveComputedUserset(ctx context.Context, userset *o } // resolveTupleToUserset creates a new leaf node containing the result of expanding a TupleToUserset rewrite. -func (query *ExpandQuery) resolveTupleToUserset(ctx context.Context, store string, userset *openfgapb.TupleToUserset, tk *openfgapb.TupleKey, metadata *utils.ResolutionMetadata) (*openfgapb.UsersetTree_Node, error) { +func (query *ExpandQuery) resolveTupleToUserset( + ctx context.Context, + store, modelID string, + userset *openfgapb.TupleToUserset, + tk *openfgapb.TupleKey, + metadata *utils.ResolutionMetadata, +) (*openfgapb.UsersetTree_Node, error) { ctx, span := query.tracer.Start(ctx, "resolveTupleToUserset") defer span.End() + targetObject := tk.GetObject() + + tupleset := userset.GetTupleset().GetRelation() tsKey := &openfgapb.TupleKey{ - Object: tk.GetObject(), - Relation: userset.GetTupleset().GetRelation(), + Object: targetObject, + Relation: tupleset, } + if tsKey.GetRelation() == "" { tsKey.Relation = tk.GetRelation() } @@ -170,6 +188,7 @@ func (query *ExpandQuery) resolveTupleToUserset(ctx context.Context, store strin return nil, serverErrors.HandleError("", err) } defer iter.Stop() + var computed []*openfgapb.UsersetTree_Computed seen := make(map[string]bool) for { @@ -181,29 +200,52 @@ func (query *ExpandQuery) resolveTupleToUserset(ctx context.Context, store strin return nil, serverErrors.HandleError("", err) } user := tuple.GetKey().GetUser() + + if user == Wildcard { + objectType, _ := tupleUtils.SplitObject(targetObject) + + query.logger.WarnWithContext( + ctx, + fmt.Sprintf("unexpected wildcard evaluated on tupleset relation '%s'", tupleset), + zap.String("store_id", store), + zap.String("authorization_model_id", modelID), + zap.String("object_type", objectType), + ) + + return nil, serverErrors.InvalidTuple( + fmt.Sprintf("unexpected wildcard evaluated on relation '%s#%s'", objectType, tupleset), + tupleUtils.NewTupleKey(targetObject, tupleset, Wildcard), + ) + } + // user must contain a type (i.e., be an object or userset) if tupleUtils.GetType(user) == "" { continue } + tObject, tRelation := tupleUtils.SplitObjectRelation(user) // We only proceed in the case that tRelation == userset.GetComputedUserset().GetRelation(). // tRelation may be empty, and in this case, we set it to userset.GetComputedUserset().GetRelation(). if tRelation == "" { tRelation = userset.GetComputedUserset().GetRelation() } + if tRelation != userset.GetComputedUserset().GetRelation() { continue } + cs := &openfgapb.TupleKey{ Object: tObject, Relation: tRelation, } + computedRelation := toObjectRelation(cs) if !seen[computedRelation] { computed = append(computed, &openfgapb.UsersetTree_Computed{Userset: computedRelation}) seen[computedRelation] = true } } + return &openfgapb.UsersetTree_Node{ Name: toObjectRelation(tk), Value: &openfgapb.UsersetTree_Node_Leaf{
server/test/check.go+99 −7 modified@@ -1167,6 +1167,104 @@ func TestCheckQuery(t *testing.T, datastore storage.OpenFGADatastore) { Allowed: true, }, }, + { + name: "Error if * encountered in TupleToUserset evaluation", + resolveNodeLimit: defaultResolveNodeLimit, + request: &openfgapb.CheckRequest{ + TupleKey: tuple.NewTupleKey("document:doc1", "viewer", "user:anne"), + }, + typeDefinitions: []*openfgapb.TypeDefinition{ + { + Type: "document", + Relations: map[string]*openfgapb.Userset{ + "parent": typesystem.This(), + "viewer": typesystem.TupleToUserset("parent", "viewer"), + }, + Metadata: &openfgapb.Metadata{ + Relations: map[string]*openfgapb.RelationMetadata{ + "parent": { + DirectlyRelatedUserTypes: []*openfgapb.RelationReference{ + typesystem.RelationReference("folder", ""), + }, + }, + }, + }, + }, + { + Type: "folder", + Relations: map[string]*openfgapb.Userset{ + "viewer": typesystem.This(), + }, + Metadata: &openfgapb.Metadata{ + Relations: map[string]*openfgapb.RelationMetadata{ + "viewer": { + DirectlyRelatedUserTypes: []*openfgapb.RelationReference{ + typesystem.RelationReference("user", ""), + }, + }, + }, + }, + }, + }, + tuples: []*openfgapb.TupleKey{ + tuple.NewTupleKey("document:doc1", "parent", "*"), // wildcard not allowed on tupleset relations + tuple.NewTupleKey("folder:folder1", "viewer", "user:anne"), + }, + err: serverErrors.InvalidTuple( + "unexpected wildcard evaluated on relation 'document#parent'", + tuple.NewTupleKey("document:doc1", "parent", commands.Wildcard), + ), + }, + { + name: "Error if * encountered in TTU evaluation including ContextualTuples", + resolveNodeLimit: defaultResolveNodeLimit, + request: &openfgapb.CheckRequest{ + TupleKey: tuple.NewTupleKey("document:doc1", "viewer", "user:anne"), + ContextualTuples: &openfgapb.ContextualTupleKeys{ + TupleKeys: []*openfgapb.TupleKey{ + tuple.NewTupleKey("document:doc1", "parent", "*"), // wildcard not allowed on tupleset relations + tuple.NewTupleKey("folder:folder1", "viewer", "user:anne"), + }, + }, + }, + typeDefinitions: []*openfgapb.TypeDefinition{ + { + Type: "document", + Relations: map[string]*openfgapb.Userset{ + "parent": typesystem.This(), + "viewer": typesystem.TupleToUserset("parent", "viewer"), + }, + Metadata: &openfgapb.Metadata{ + Relations: map[string]*openfgapb.RelationMetadata{ + "parent": { + DirectlyRelatedUserTypes: []*openfgapb.RelationReference{ + typesystem.RelationReference("folder", ""), + }, + }, + }, + }, + }, + { + Type: "folder", + Relations: map[string]*openfgapb.Userset{ + "viewer": typesystem.This(), + }, + Metadata: &openfgapb.Metadata{ + Relations: map[string]*openfgapb.RelationMetadata{ + "viewer": { + DirectlyRelatedUserTypes: []*openfgapb.RelationReference{ + typesystem.RelationReference("user", ""), + }, + }, + }, + }, + }, + }, + err: serverErrors.InvalidTuple( + "unexpected wildcard evaluated on relation 'document#parent'", + tuple.NewTupleKey("document:doc1", "parent", commands.Wildcard), + ), + }, } ctx := context.Background() @@ -1196,13 +1294,7 @@ func TestCheckQuery(t *testing.T, datastore storage.OpenFGADatastore) { test.request.AuthorizationModelId = model.Id resp, gotErr := cmd.Execute(ctx, test.request) - if test.err == nil { - require.NoError(t, gotErr) - } - - if test.err != nil { - require.EqualError(t, test.err, gotErr.Error()) - } + require.ErrorIs(t, gotErr, test.err) if test.response != nil { require.NoError(t, gotErr)
server/test/expand.go+27 −4 modified@@ -4,12 +4,12 @@ import ( "context" "testing" - "github.com/go-errors/errors" "github.com/google/go-cmp/cmp" "github.com/openfga/openfga/pkg/id" "github.com/openfga/openfga/pkg/logger" "github.com/openfga/openfga/pkg/telemetry" "github.com/openfga/openfga/pkg/testutils" + "github.com/openfga/openfga/pkg/tuple" "github.com/openfga/openfga/pkg/typesystem" "github.com/openfga/openfga/server/commands" serverErrors "github.com/openfga/openfga/server/errors" @@ -811,6 +811,31 @@ func TestExpandQueryErrors(t *testing.T, datastore storage.OpenFGADatastore) { Relation: "baz", }), }, + { + name: "TupleToUserset involving wildcard returns error", + typeDefinitions: []*openfgapb.TypeDefinition{ + { + Type: "document", + Relations: map[string]*openfgapb.Userset{ + "parent": typesystem.This(), + "viewer": typesystem.Union( + typesystem.This(), typesystem.TupleToUserset("parent", "viewer"), + ), + }, + }, + }, + tuples: []*openfgapb.TupleKey{ + tuple.NewTupleKey("document:1", "parent", "*"), + tuple.NewTupleKey("document:X", "viewer", "jon"), + }, + request: &openfgapb.ExpandRequest{ + TupleKey: tuple.NewTupleKey("document:1", "viewer", ""), + }, + expected: serverErrors.InvalidTuple( + "unexpected wildcard evaluated on relation 'document#parent'", + tuple.NewTupleKey("document:1", "parent", "*"), + ), + }, } require := require.New(t) @@ -830,9 +855,7 @@ func TestExpandQueryErrors(t *testing.T, datastore storage.OpenFGADatastore) { test.request.AuthorizationModelId = modelID _, err = query.Execute(ctx, test.request) - if !errors.Is(err, test.expected) { - t.Fatalf("'%s': Execute(), err = %v, want %v", test.name, err, test.expected) - } + require.ErrorIs(err, test.expected) }) } }
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-vj4m-83m8-xpw5ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-39341ghsaADVISORY
- github.com/openfga/openfga/commit/b466769cc100b2065047786578718d313f52695bghsaWEB
- github.com/openfga/openfga/releases/tag/v0.2.4ghsaWEB
- github.com/openfga/openfga/security/advisories/GHSA-vj4m-83m8-xpw5ghsaWEB
News mentions
0No linked articles in our index yet.