CVE-2022-39342 (published 2022-10-25T17:15Z)

OpenFGA is an authorization/permission engine. Versions prior to version 0.2.4 are vulnerable to authorization bypass under certain conditions. Users whose model has a relation defined as a tupleset (the right hand side of a ‘from’ statement) that involves anything other than a direct relationship (e.g. ‘as self’) are vulnerable. Version 0.2.4 contains a patch for this issue.

CVSS v3 base score: 9.8, CRITICAL (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: NVD-CWE-Other
cwe_name: Other
description: NVD is only using a subset of CWE for mapping instead of the entire CWE, and the weakness type is not covered by that subset.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/openfga/openfga
commit_details: repo_url: https://github.com/openfga/openfga
author: Jonathan Whitaker
author_date: 2022-10-21 10:18:07-06:00
msg: Merge pull request from GHSA-f4mm-2r69-mg5f
repository: repo_url: https://github.com/openfga/openfga
repo_name: openfga/openfga
description: A high performance and flexible authorization/permission engine built for developers and inspired by Google Zanzibar
repo_language: Go
owner: openfga
file_changes: filename: typesystem.go
change_type: ModificationType.MODIFY
diff: @@ -2,6 +2,7 @@ package typesystem
 
 import (
 	"fmt"
+	"reflect"
 
 	"github.com/go-errors/errors"
 	openfgapb "go.buf.build/openfga/go/openfga/api/openfga/v1"
@@ -233,21 +234,41 @@ func containsDuplicateType(model *openfgapb.AuthorizationModel) bool {
 func validateRelationRewrites(model *openfgapb.AuthorizationModel) error {
 	typeDefinitions := model.GetTypeDefinitions()
 
-	allRelations := map[string]struct{}{}
-	typeToRelations := map[string]map[string]struct{}{}
+	relations := map[string]*openfgapb.Relation{}
+	typerels := map[string]map[string]*openfgapb.Relation{}
+
 	for _, td := range typeDefinitions {
 		objectType := td.GetType()
-		typeToRelations[objectType] = map[string]struct{}{}
-		for relation := range td.GetRelations() {
-			typeToRelations[objectType][relation] = struct{}{}
-			allRelations[relation] = struct{}{}
+
+		typerels[objectType] = map[string]*openfgapb.Relation{}
+
+		for relation, rewrite := range td.GetRelations() {
+			relationMetadata := td.GetMetadata().GetRelations()
+			md, ok := relationMetadata[relation]
+
+			var typeinfo *openfgapb.RelationTypeInfo
+			if ok {
+				typeinfo = &openfgapb.RelationTypeInfo{
+					DirectlyRelatedUserTypes: md.GetDirectlyRelatedUserTypes(),
+				}
+			}
+
+			r := &openfgapb.Relation{
+				Name:     relation,
+				Rewrite:  rewrite,
+				TypeInfo: typeinfo,
+			}
+
+			typerels[objectType][relation] = r
+			relations[relation] = r
 		}
 	}
 
 	for _, td := range typeDefinitions {
 		objectType := td.GetType()
+
 		for relation, rewrite := range td.GetRelations() {
-			err := isUsersetRewriteValid(allRelations, typeToRelations[objectType], objectType, relation, rewrite)
+			err := isUsersetRewriteValid(relations, typerels[objectType], objectType, relation, rewrite)
 			if err != nil {
 				return err
 			}
@@ -259,7 +280,12 @@ func validateRelationRewrites(model *openfgapb.AuthorizationModel) error {
 
 // isUsersetRewriteValid checks if a particular userset rewrite is valid. The first argument is all the relations in
 // the typeSystem, the second argument is the subset of relations on the type where the rewrite occurs.
-func isUsersetRewriteValid(allRelations map[string]struct{}, relationsOnType map[string]struct{}, objectType, relation string, rewrite *openfgapb.Userset) error {
+func isUsersetRewriteValid(
+	allRelations map[string]*openfgapb.Relation,
+	relationsOnType map[string]*openfgapb.Relation,
+	objectType, relation string,
+	rewrite *openfgapb.Userset,
+) error {
 	if rewrite.GetUserset() == nil {
 		return InvalidRelationError(objectType, relation)
 	}
@@ -275,10 +301,19 @@ func isUsersetRewriteValid(allRelations map[string]struct{}, relationsOnType map
 		}
 	case *openfgapb.Userset_TupleToUserset:
 		tupleset := t.TupleToUserset.GetTupleset().GetRelation()
-		if _, ok := relationsOnType[tupleset]; !ok {
+
+		tuplesetRelation, ok := relationsOnType[tupleset]
+		if !ok {
 			return RelationDoesNotExistError(objectType, tupleset)
 		}
 
+		// tupleset relations must only be direct relationships, no rewrites
+		// are allowed on them
+		tuplesetRewrite := tuplesetRelation.GetRewrite()
+		if reflect.TypeOf(tuplesetRewrite.GetUserset()) != reflect.TypeOf(&openfgapb.Userset_This{}) {
+			return errors.Errorf("the '%s#%s' relation is referenced in at least one tupleset and thus must be a direct relation", objectType, tupleset)
+		}
+
 		computedUserset := t.TupleToUserset.GetComputedUserset().GetRelation()
 		if _, ok := allRelations[computedUserset]; !ok {
 			return RelationDoesNotExistError("", computedUserset)

programming_language: Go
filename: write_authzmodel.go
change_type: ModificationType.MODIFY
diff: @@ -5,11 +5,12 @@ import (
 	"fmt"
 	"testing"
 
+	"github.com/go-errors/errors"
 	"github.com/openfga/openfga/pkg/id"
 	"github.com/openfga/openfga/pkg/logger"
 	"github.com/openfga/openfga/pkg/typesystem"
 	"github.com/openfga/openfga/server/commands"
-	"github.com/openfga/openfga/server/errors"
+	serverErrors "github.com/openfga/openfga/server/errors"
 	"github.com/openfga/openfga/storage"
 	"github.com/stretchr/testify/require"
 	openfgapb "go.buf.build/openfga/go/openfga/api/openfga/v1"
@@ -109,7 +110,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor
 				StoreId:         storeID,
 				TypeDefinitions: items,
 			},
-			err: errors.ExceededEntityLimit("type definitions in an authorization model", datastore.MaxTypesInTypeDefinition()),
+			err: serverErrors.ExceededEntityLimit("type definitions in an authorization model", datastore.MaxTypesInTypeDefinition()),
 		},
 		{
 			name: "empty relations is valid",
@@ -151,7 +152,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor
 					},
 				},
 			},
-			err: errors.InvalidAuthorizationModelInput(typesystem.ErrDuplicateTypes),
+			err: serverErrors.InvalidAuthorizationModelInput(typesystem.ErrDuplicateTypes),
 		},
 		{
 			name: "ExecuteWriteFailsIfEmptyRewrites",
@@ -166,7 +167,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor
 					},
 				},
 			},
-			err: errors.InvalidAuthorizationModelInput(typesystem.InvalidRelationError("repo", "owner")),
+			err: serverErrors.InvalidAuthorizationModelInput(typesystem.InvalidRelationError("repo", "owner")),
 		},
 		{
 			name: "ExecuteWriteFailsIfUnknownRelationInComputedUserset",
@@ -188,7 +189,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor
 					},
 				},
 			},
-			err: errors.InvalidAuthorizationModelInput(typesystem.RelationDoesNotExistError("repo", "owner")),
+			err: serverErrors.InvalidAuthorizationModelInput(typesystem.RelationDoesNotExistError("repo", "owner")),
 		},
 		{
 			name: "ExecuteWriteFailsIfUnknownRelationInTupleToUserset",
@@ -215,7 +216,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor
 					},
 				},
 			},
-			err: errors.InvalidAuthorizationModelInput(typesystem.RelationDoesNotExistError("", "owner")),
+			err: serverErrors.InvalidAuthorizationModelInput(typesystem.RelationDoesNotExistError("", "owner")),
 		},
 		{
 			name: "ExecuteWriteFailsIfUnknownRelationInUnion",
@@ -250,7 +251,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor
 					},
 				},
 			},
-			err: errors.InvalidAuthorizationModelInput(typesystem.RelationDoesNotExistError("repo", "owner")),
+			err: serverErrors.InvalidAuthorizationModelInput(typesystem.RelationDoesNotExistError("repo", "owner")),
 		},
 		{
 			name: "ExecuteWriteFailsIfUnknownRelationInDifferenceBaseArgument",
@@ -285,7 +286,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor
 					},
 				},
 			},
-			err: errors.InvalidAuthorizationModelInput(typesystem.RelationDoesNotExistError("repo", "owner")),
+			err: serverErrors.InvalidAuthorizationModelInput(typesystem.RelationDoesNotExistError("repo", "owner")),
 		},
 		{
 			name: "ExecuteWriteFailsIfUnknownRelationInDifferenceSubtractArgument",
@@ -320,7 +321,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor
 					},
 				},
 			},
-			err: errors.InvalidAuthorizationModelInput(typesystem.RelationDoesNotExistError("repo", "owner")),
+			err: serverErrors.InvalidAuthorizationModelInput(typesystem.RelationDoesNotExistError("repo", "owner")),
 		},
 		{
 			name: "ExecuteWriteFailsIfUnknownRelationInTupleToUsersetTupleset",
@@ -349,7 +350,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor
 					},
 				},
 			},
-			err: errors.InvalidAuthorizationModelInput(typesystem.RelationDoesNotExistError("repo", "owner")),
+			err: serverErrors.InvalidAuthorizationModelInput(typesystem.RelationDoesNotExistError("repo", "owner")),
 		},
 		{
 			name: "ExecuteWriteFailsIfUnknownRelationInTupleToUsersetComputedUserset",
@@ -378,7 +379,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor
 					},
 				},
 			},
-			err: errors.InvalidAuthorizationModelInput(typesystem.RelationDoesNotExistError("", "owner")),
+			err: serverErrors.InvalidAuthorizationModelInput(typesystem.RelationDoesNotExistError("", "owner")),
 		},
 		{
 			name: "ExecuteWriteFailsIfTupleToUsersetReferencesUnknownRelation",
@@ -414,7 +415,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor
 					},
 				},
 			},
-			err: errors.InvalidAuthorizationModelInput(typesystem.RelationDoesNotExistError("bar", "writer")),
+			err: serverErrors.InvalidAuthorizationModelInput(typesystem.RelationDoesNotExistError("bar", "writer")),
 		},
 		{
 			name: "ExecuteWriteFailsIfUnknownRelationInIntersection",
@@ -447,7 +448,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor
 					},
 				},
 			},
-			err: errors.InvalidAuthorizationModelInput(typesystem.RelationDoesNotExistError("repo", "owner")),
+			err: serverErrors.InvalidAuthorizationModelInput(typesystem.RelationDoesNotExistError("repo", "owner")),
 		},
 		{
 			name: "ExecuteWriteFailsIfDifferenceIncludesSameRelationTwice",
@@ -477,7 +478,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor
 					},
 				},
 			},
-			err: errors.InvalidAuthorizationModelInput(typesystem.InvalidRelationError("repo", "viewer")),
+			err: serverErrors.InvalidAuthorizationModelInput(typesystem.InvalidRelationError("repo", "viewer")),
 		},
 		{
 			name: "ExecuteWriteFailsIfUnionIncludesSameRelationTwice",
@@ -504,7 +505,7 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor
 					},
 				},
 			},
-			err: errors.InvalidAuthorizationModelInput(typesystem.InvalidRelationError("repo", "viewer")),
+			err: serverErrors.InvalidAuthorizationModelInput(typesystem.InvalidRelationError("repo", "viewer")),
 		},
 		{
 			name: "ExecuteWriteFailsIfIntersectionIncludesSameRelationTwice",
@@ -530,7 +531,171 @@ func WriteAuthorizationModelTest(t *testing.T, datastore storage.OpenFGADatastor
 					},
 				},
 			},
-			err: errors.InvalidAuthorizationModelInput(typesystem.InvalidRelationError("repo", "viewer")),
+			err: serverErrors.InvalidAuthorizationModelInput(typesystem.InvalidRelationError("repo", "viewer")),
+		},
+		{
+			name: "Union Rewrite Contains Repeated Definitions",
+			request: &openfgapb.WriteAuthorizationModelRequest{
+				StoreId: storeID,
+				TypeDefinitions: []*openfgapb.TypeDefinition{
+					{
+						Type: "document",
+						Relations: map[string]*openfgapb.Userset{
+							"parent": typesystem.This(),
+							"viewer": typesystem.Union(
+								typesystem.ComputedUserset("editor"),
+								typesystem.ComputedUserset("editor"),
+							),
+							"editor": typesystem.Union(typesystem.This(), typesystem.This()),
+							"manage": typesystem.Union(
+								typesystem.TupleToUserset("parent", "manage"),
+								typesystem.TupleToUserset("parent", "manage"),
+							),
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "Intersection Rewrite Contains Repeated Definitions",
+			request: &openfgapb.WriteAuthorizationModelRequest{
+				StoreId: storeID,
+				TypeDefinitions: []*openfgapb.TypeDefinition{
+					{
+						Type: "document",
+						Relations: map[string]*openfgapb.Userset{
+							"parent": typesystem.This(),
+							"viewer": typesystem.Intersection(
+								typesystem.ComputedUserset("editor"),
+								typesystem.ComputedUserset("editor"),
+							),
+							"editor": typesystem.Intersection(typesystem.This(), typesystem.This()),
+							"manage": typesystem.Intersection(
+								typesystem.TupleToUserset("parent", "manage"),
+								typesystem.TupleToUserset("parent", "manage"),
+							),
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "Exclusion Rewrite Contains Repeated Definitions",
+			request: &openfgapb.WriteAuthorizationModelRequest{
+				StoreId: storeID,
+				TypeDefinitions: []*openfgapb.TypeDefinition{
+					{
+						Type: "document",
+						Relations: map[string]*openfgapb.Userset{
+							"parent": typesystem.This(),
+							"viewer": typesystem.Difference(
+								typesystem.ComputedUserset("editor"),
+								typesystem.ComputedUserset("editor"),
+							),
+							"editor": typesystem.Difference(typesystem.This(), typesystem.This()),
+							"manage": typesystem.Difference(
+								typesystem.TupleToUserset("parent", "manage"),
+								typesystem.TupleToUserset("parent", "manage"),
+							),
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "Tupleset relation involves ComputedUserset rewrite",
+			request: &openfgapb.WriteAuthorizationModelRequest{
+				StoreId: storeID,
+				TypeDefinitions: []*openfgapb.TypeDefinition{
+					{
+						Type: "document",
+						Relations: map[string]*openfgapb.Userset{
+							"ancestor": typesystem.This(),
+							"parent":   typesystem.ComputedUserset("ancestor"),
+							"viewer":   typesystem.TupleToUserset("parent", "viewer"),
+						},
+					},
+				},
+			},
+			err: serverErrors.InvalidAuthorizationModelInput(
+				errors.New("the 'document#parent' relation is referenced in at least one tupleset and thus must be a direct relation"),
+			),
+		},
+		{
+			name: "Tupleset relation involves Union rewrite",
+			request: &openfgapb.WriteAuthorizationModelRequest{
+				StoreId: storeID,
+				TypeDefinitions: []*openfgapb.TypeDefinition{
+					{
+						Type: "document",
+						Relations: map[string]*openfgapb.Userset{
+							"ancestor": typesystem.This(),
+							"parent":   typesystem.Union(typesystem.This(), typesystem.ComputedUserset("ancestor")),
+							"viewer":   typesystem.TupleToUserset("parent", "viewer"),
+						},
+					},
+				},
+			},
+			err: serverErrors.InvalidAuthorizationModelInput(
+				errors.New("the 'document#parent' relation is referenced in at least one tupleset and thus must be a direct relation"),
+			),
+		},
+		{
+			name: "Tupleset relation involves Intersection rewrite",
+			request: &openfgapb.WriteAuthorizationModelRequest{
+				StoreId: storeID,
+				TypeDefinitions: []*openfgapb.TypeDefinition{
+					{
+						Type: "document",
+						Relations: map[string]*openfgapb.Userset{
+							"ancestor": typesystem.This(),
+							"parent":   typesystem.Intersection(typesystem.This(), typesystem.ComputedUserset("ancestor")),
+							"viewer":   typesystem.TupleToUserset("parent", "viewer"),
+						},
+					},
+				},
+			},
+			err: serverErrors.InvalidAuthorizationModelInput(
+				errors.New("the 'document#parent' relation is referenced in at least one tupleset and thus must be a direct relation"),
+			),
+		},
+		{
+			name: "Tupleset relation involves Exclusion rewrite",
+			request: &openfgapb.WriteAuthorizationModelRequest{
+				StoreId: storeID,
+				TypeDefinitions: []*openfgapb.TypeDefinition{
+					{
+						Type: "document",
+						Relations: map[string]*openfgapb.Userset{
+							"restricted": typesystem.This(),
+							"parent":     typesystem.Difference(typesystem.This(), typesystem.ComputedUserset("restricted")),
+							"viewer":     typesystem.TupleToUserset("parent", "viewer"),
+						},
+					},
+				},
+			},
+			err: serverErrors.InvalidAuthorizationModelInput(
+				errors.New("the 'document#parent' relation is referenced in at least one tupleset and thus must be a direct relation"),
+			),
+		},
+		{
+			name: "Tupleset relation involves TupleToUserset rewrite",
+			request: &openfgapb.WriteAuthorizationModelRequest{
+				StoreId: storeID,
+				TypeDefinitions: []*openfgapb.TypeDefinition{
+					{
+						Type: "document",
+						Relations: map[string]*openfgapb.Userset{
+							"ancestor": typesystem.This(),
+							"parent":   typesystem.TupleToUserset("ancestor", "viewer"),
+							"viewer":   typesystem.TupleToUserset("parent", "viewer"),
+						},
+					},
+				},
+			},
+			err: serverErrors.InvalidAuthorizationModelInput(
+				errors.New("the 'document#parent' relation is referenced in at least one tupleset and thus must be a direct relation"),
+			),
 		},
 	}
 

programming_language: Go