VYPR
Low severityNVD Advisory· Published Jun 6, 2025· Updated Jun 6, 2025

SpiceDB checks involving relations with caveats can result in no permission when permission is expected

CVE-2025-49011

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.

PackageAffected versionsPatched versions
github.com/authzed/spicedbGo
< 1.44.21.44.2

Affected products

1

Patches

1
fe8dd9f491f6

fix: Checks involving relations with caveats can result in no permission when permission is expected

https://github.com/authzed/spicedbMaria Ines ParnisariJun 5, 2025via ghsa
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

News mentions

0

No linked articles in our index yet.