SpiceDB checks involving relations with caveats can result in no permission when permission is expected
Description
SpiceDB is an open source database for storing and querying fine-grained authorization data. Prior to version 1.44.2, on schemas involving arrows with caveats on the arrow’ed relation, when the path to resolve a CheckPermission request involves the evaluation of multiple caveated branches, requests may return a negative response when a positive response is expected. Version 1.44.2 fixes the issue. As a workaround, do not use caveats in the schema over an arrow’ed relation.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/authzed/spicedbGo | < 1.44.2 | 1.44.2 |
Affected products
1Patches
1fe8dd9f491f6fix: Checks involving relations with caveats can result in no permission when permission is expected
5 files changed · +172 −2
internal/developmentmembership/membership.go+2 −1 modified@@ -3,6 +3,7 @@ package developmentmembership import ( "fmt" + "github.com/authzed/spicedb/internal/datasets" core "github.com/authzed/spicedb/pkg/proto/core/v1" "github.com/authzed/spicedb/pkg/spiceerrors" "github.com/authzed/spicedb/pkg/tuple" @@ -83,7 +84,7 @@ func populateFoundSubjects(rootONR tuple.ObjectAndRelation, treeNode *core.Relat case core.SetOperationUserset_INTERSECTION: if len(typed.IntermediateNode.ChildNodes) == 0 { - return nil, fmt.Errorf("found intersection with no children") + return &TrackingSubjectSet{setByType: make(map[tuple.RelationReference]datasets.BaseSubjectSet[FoundSubject])}, nil } firstChildSet, err := populateFoundSubjects(rootONR, typed.IntermediateNode.ChildNodes[0])
internal/dispatch/graph/check_test.go+91 −1 modified@@ -1320,6 +1320,96 @@ func TestCheckPermissionOverSchema(t *testing.T) { caveatAndCtx("somecaveat", map[string]any{"somevalue": int64(40)}), ), }, + { + name: "caveated_with_arrows", + schema: ` + definition user {} + + definition office { + relation parent: office + relation manager: user + permission read = manager + parent->read + } + + definition group { + relation parent: office + permission read = parent->read + } + + definition document { + relation owner: group with equals + permission read = owner->read + } + + caveat equals(actual string, required string) { + actual == required + } + `, + relationships: []tuple.Relationship{ + tuple.MustParse(`office:headoffice#manager@user:maria`), + tuple.MustParse(`office:branch1#parent@office:headoffice`), + tuple.MustParse(`group:admins#parent@office:branch1`), + tuple.MustParse(`group:managers#parent@office:headoffice`), + tuple.MustParse(`document:budget#owner@group:admins[equals:{"required":"admin"}]`), + tuple.MustParse(`document:budget#owner@group:managers[equals:{"required":"manager"}]`), + }, + resource: ONR("document", "budget", "read"), + subject: ONR("user", "maria", "..."), + expectedPermissionship: v1.ResourceCheckResult_CAVEATED_MEMBER, + expectedCaveat: caveatOr( + caveatAndCtx("equals", map[string]any{"required": "admin"}), + caveatAndCtx("equals", map[string]any{"required": "manager"}), + ), + alternativeExpectedCaveat: caveatOr( + caveatAndCtx("equals", map[string]any{"required": "manager"}), + caveatAndCtx("equals", map[string]any{"required": "admin"}), + ), + }, + { + name: "caveated_nested_with_intersection_arrows", + schema: ` + definition user {} + + definition office { + relation parent: office + relation manager: user + permission read = manager + parent.all(read) + } + + definition group { + relation parent: office + permission read = parent.all(read) + } + + definition document { + relation owner: group with equals + permission read = owner.all(read) + } + + caveat equals(actual string, required string) { + actual == required + } + `, + relationships: []tuple.Relationship{ + tuple.MustParse(`office:headoffice#manager@user:maria`), + tuple.MustParse(`office:branch1#parent@office:headoffice`), + tuple.MustParse(`group:admins#parent@office:branch1`), + tuple.MustParse(`group:managers#parent@office:headoffice`), + tuple.MustParse(`document:budget#owner@group:admins[equals:{"required":"admin"}]`), + tuple.MustParse(`document:budget#owner@group:managers[equals:{"required":"manager"}]`), + }, + resource: ONR("document", "budget", "read"), + subject: ONR("user", "maria", "..."), + expectedPermissionship: v1.ResourceCheckResult_CAVEATED_MEMBER, + expectedCaveat: caveatAnd( + caveatAndCtx("equals", map[string]any{"required": "admin"}), + caveatAndCtx("equals", map[string]any{"required": "manager"}), + ), + alternativeExpectedCaveat: caveatAnd( + caveatAndCtx("equals", map[string]any{"required": "manager"}), + caveatAndCtx("equals", map[string]any{"required": "admin"}), + ), + }, } for _, tc := range testCases { @@ -1356,7 +1446,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { membership = r.Membership } - require.Equal(tc.expectedPermissionship, membership) + require.Equal(tc.expectedPermissionship, membership, fmt.Sprintf("expected permissionship %s, got %s", tc.expectedPermissionship, membership)) if tc.expectedCaveat != nil && tc.alternativeExpectedCaveat == nil { require.NotEmpty(resp.ResultsByResourceId[tc.resource.ObjectID].Expression)
internal/graph/check.go+3 −0 modified@@ -988,6 +988,9 @@ func checkTupleToUserset[T relation]( toDispatch, func(ctx context.Context, crc currentRequestContext, dd checkDispatchChunk) CheckResult { resourceType := dd.resourceType + if dd.hasIncomingCaveats { + crc.resultsSetting = v1.DispatchCheckRequest_REQUIRE_ALL_RESULTS + } childResult := cc.checkComputedUserset(ctx, crc, ttu.GetComputedUserset(), &resourceType, dd.resourceIds) if childResult.Err != nil { return childResult
internal/services/integrationtesting/testconfigs/multibranchcaveatwitharrows.yaml+37 −0 added@@ -0,0 +1,37 @@ +--- +schema: |+ + definition user {} + + definition office { + relation parent: office + relation manager: user + permission read = manager + parent->read + } + + definition group { + relation parent: office + permission read = parent->read + } + + definition document { + relation owner: group with equals + permission read = owner->read + } + + caveat equals(actual string, required string) { + actual == required + } + +relationships: | + office:headoffice#manager@user:maria + office:branch1#parent@office:headoffice + group:admins#parent@office:branch1 + group:managers#parent@office:headoffice + document:budget#owner@group:admins[equals:{"required":"admin"}] + document:budget#owner@group:managers[equals:{"required":"manager"}] +assertions: + assertTrue: + - 'document:budget#read@user:maria with {"actual" : "admin"}' + - 'document:budget#read@user:maria with {"actual" : "manager"}' + assertFalse: + - 'document:budget#read@user:maria with {"actual" : "unknown"}'
internal/services/integrationtesting/testconfigs/multibranchcaveatwithintersectionarrows.yaml+39 −0 added@@ -0,0 +1,39 @@ +--- +schema: |+ + definition user {} + + definition office { + relation parent: office + relation manager: user + permission read = manager + parent.all(read) + } + + definition group { + relation parent: office + permission read = parent.all(read) + } + + definition document { + relation owner: group with equals + permission read = owner.all(read) + } + + caveat equals(actual string, required string) { + actual == required + } + +relationships: | + office:headoffice#manager@user:maria + office:branch1#manager@user:maria + office:branch1#parent@office:headoffice + group:admins#parent@office:branch1 + group:managers#parent@office:headoffice + document:budget#owner@group:admins[equals:{"required":"admin"}] + document:budget#owner@group:managers[equals:{"required":"admin"}] +assertions: + assertTrue: + - 'document:budget#read@user:maria with {"actual" : "admin"}' + assertFalse: + - 'document:budget#read@user:maria with {"actual" : "unknown"}' + - 'document:unknown#read@user:maria with {"actual" : "admin"}' + - 'document:budget#read@user:unknown with {"actual" : "admin"}'
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-cwwm-hr97-qfxmghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-49011ghsaADVISORY
- github.com/authzed/spicedb/commit/fe8dd9f491f6975b3408c401e413a530eb181a67ghsax_refsource_MISCWEB
- github.com/authzed/spicedb/releases/tag/v1.44.2ghsax_refsource_MISCWEB
- github.com/authzed/spicedb/security/advisories/GHSA-cwwm-hr97-qfxmghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.