CVE-2026-10814
Description
A vulnerability has been found in milvus-io milvus up to 2.6.13. This vulnerability affects unknown code of the file internal/metastore/kv/rootcoord/kv_catalog.go of the component Grantee ID Hash Handler. The manipulation leads to use of weak hash. The attack needs to be performed locally. The attack's complexity is rated as high. It is stated that the exploitability is difficult. The exploit has been disclosed to the public and may be used. The identifier of the patch is 3d932f1c3e065351c4440c27abe1e6479752544d. Applying a patch is the recommended action to fix this issue.
Affected products
2Patches
13d932f1c3e06fix: [RBAC] use full-length grantee ID hash (#50060)
4 files changed · +721 −35
internal/metastore/kv/rootcoord/kv_catalog.go+255 −29 modified@@ -1332,46 +1332,224 @@ func (kc *Catalog) ListUser(ctx context.Context, tenant string, entity *milvuspb return results, nil } +func buildGranteeIDKey(idStr string, privilegeName string) string { + return fmt.Sprintf("%s/%s/%s", GranteeIDPrefix, idStr, privilegeName) +} + +func granteeIDCandidates(granteeKey string, idStr string) []string { + candidates := make([]string, 0, 2) + seen := make(map[string]struct{}, 2) + appendCandidate := func(candidate string) { + if candidate == "" { + return + } + if _, ok := seen[candidate]; ok { + return + } + seen[candidate] = struct{}{} + candidates = append(candidates, candidate) + } + + newID := crypto.GranteeID(granteeKey) + appendCandidate(idStr) + if idStr != newID { + appendCandidate(newID) + } + return candidates +} + +func newSharedGranteeIDError(idStr string, granteeKey string, otherGranteeKey string) error { + return merr.WrapErrIoFailedReason(fmt.Sprintf("shared legacy grantee id %s is referenced by both %s and %s", idStr, granteeKey, otherGranteeKey)) +} + +func isLegacyGranteeID(idStr string) bool { + return len(idStr) == 16 +} + +func logicalGranteeKeyFromEtcdKey(ctx context.Context, granteePrefix string, key string) (string, bool) { + grantInfos := typeutil.AfterN(key, granteePrefix, "/") + if len(grantInfos) != 3 { + log.Ctx(ctx).Warn("invalid grantee key while checking grantee id sharing", + zap.String("key", key), zap.String("prefix", granteePrefix)) + return "", false + } + return fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, grantInfos[0], grantInfos[1], grantInfos[2]), true +} + +func findOtherGranteeWithIDFromKeys(ctx context.Context, granteePrefix string, granteeKey string, idStr string, keys []string, values []string) (string, error) { + for i, key := range keys { + if i >= len(values) { + log.Ctx(ctx).Warn("grantee key has no matching id value while checking grantee id sharing", + zap.String("key", key), zap.String("prefix", granteePrefix)) + continue + } + if values[i] != idStr { + continue + } + logicalKey, ok := logicalGranteeKeyFromEtcdKey(ctx, granteePrefix, key) + if !ok { + return "", newSharedGranteeIDError(idStr, granteeKey, key) + } + if logicalKey != granteeKey { + return logicalKey, nil + } + } + return "", nil +} + +func shouldRemoveGranteeIDSubtree(ctx context.Context, granteePrefix string, idStr string, keys []string, values []string, removingGrantees map[string]struct{}) bool { + if !isLegacyGranteeID(idStr) { + return true + } + for i, key := range keys { + if i >= len(values) || values[i] != idStr { + continue + } + logicalKey, ok := logicalGranteeKeyFromEtcdKey(ctx, granteePrefix, key) + if !ok { + return false + } + if _, removing := removingGrantees[logicalKey]; !removing { + return false + } + } + return true +} + +func (kc *Catalog) loadGranteeIDPrefix(ctx context.Context, tenant string, granteeKey string, idStr string) ([]string, []string, string, error) { + var firstPrefix string + newID := crypto.GranteeID(granteeKey) + if isLegacyGranteeID(idStr) && idStr != newID { + granteeIDKey := funcutil.HandleTenantForEtcdPrefix(GranteeIDPrefix, tenant, idStr) + otherGranteeKey, err := kc.findOtherGranteeWithID(ctx, tenant, granteeKey, idStr) + if err != nil { + return nil, nil, granteeIDKey, err + } + if otherGranteeKey != "" { + return nil, nil, granteeIDKey, newSharedGranteeIDError(idStr, granteeKey, otherGranteeKey) + } + } + + for _, candidate := range granteeIDCandidates(granteeKey, idStr) { + granteeIDKey := funcutil.HandleTenantForEtcdPrefix(GranteeIDPrefix, tenant, candidate) + if firstPrefix == "" { + firstPrefix = granteeIDKey + } + keys, values, err := kc.Txn.LoadWithPrefix(ctx, granteeIDKey) + if err != nil { + if errors.Is(err, merr.ErrIoKeyNotFound) { + continue + } + return nil, nil, granteeIDKey, err + } + if len(keys) > 0 { + return keys, values, granteeIDKey, nil + } + } + return nil, nil, firstPrefix, nil +} + +func (kc *Catalog) findOtherGranteeWithID(ctx context.Context, tenant string, granteeKey string, idStr string) (string, error) { + granteePrefix := funcutil.HandleTenantForEtcdPrefix(GranteePrefix, tenant) + keys, values, err := kc.Txn.LoadWithPrefix(ctx, granteePrefix) + if err != nil { + if errors.Is(err, merr.ErrIoKeyNotFound) { + return "", nil + } + return "", err + } + return findOtherGranteeWithIDFromKeys(ctx, granteePrefix, granteeKey, idStr, keys, values) +} + +func (kc *Catalog) migrateGranteeID(ctx context.Context, tenant string, granteeKey string, idStr string) (string, error) { + newID := crypto.GranteeID(granteeKey) + if idStr == newID { + return idStr, nil + } + if isLegacyGranteeID(idStr) { + otherGranteeKey, err := kc.findOtherGranteeWithID(ctx, tenant, granteeKey, idStr) + if err != nil { + return "", err + } + if otherGranteeKey != "" { + return "", newSharedGranteeIDError(idStr, granteeKey, otherGranteeKey) + } + } + + saves := map[string]string{granteeKey: newID} + var removals []string + granteeIDKey := funcutil.HandleTenantForEtcdPrefix(GranteeIDPrefix, tenant, idStr) + keys, values, err := kc.Txn.LoadWithPrefix(ctx, granteeIDKey) + if err != nil { + if !errors.Is(err, merr.ErrIoKeyNotFound) { + return "", err + } + } + for i, key := range keys { + privilegeName := typeutil.After(key, granteeIDKey) + if privilegeName == "" { + log.Ctx(ctx).Warn("failed to extract privilege name from grantee id key", + zap.String("idKey", key), zap.String("prefix", granteeIDKey)) + continue + } + saves[buildGranteeIDKey(newID, privilegeName)] = values[i] + removals = append(removals, buildGranteeIDKey(idStr, privilegeName)) + } + if err := kc.Txn.MultiSaveAndRemove(ctx, saves, removals); err != nil { + return "", err + } + return newID, nil +} + func (kc *Catalog) AlterGrant(ctx context.Context, tenant string, entity *milvuspb.GrantEntity, operateType milvuspb.OperatePrivilegeType) error { var ( privilegeName = entity.Grantor.Privilege.Name - k = fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, entity.Role.Name, entity.Object.Name, funcutil.CombineObjectName(entity.DbName, entity.ObjectName)) + granteeKey = fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, entity.Role.Name, entity.Object.Name, funcutil.CombineObjectName(entity.DbName, entity.ObjectName)) idStr string v string err error ) // Compatible with logic without db if entity.DbName == util.DefaultDBName { - v, err = kc.Txn.Load(ctx, fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, entity.Role.Name, entity.Object.Name, entity.ObjectName)) + legacyKey := fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, entity.Role.Name, entity.Object.Name, entity.ObjectName) + v, err = kc.Txn.Load(ctx, legacyKey) if err == nil { idStr = v + granteeKey = legacyKey } } if idStr == "" { - if v, err = kc.Txn.Load(ctx, k); err == nil { + if v, err = kc.Txn.Load(ctx, granteeKey); err == nil { idStr = v } else { - log.Ctx(ctx).Warn("fail to load grant privilege entity", zap.String("key", k), zap.Any("type", operateType), zap.Error(err)) + log.Ctx(ctx).Warn("fail to load grant privilege entity", zap.String("key", granteeKey), zap.Any("type", operateType), zap.Error(err)) if funcutil.IsRevoke(operateType) { if errors.Is(err, merr.ErrIoKeyNotFound) { - return common.NewIgnorableError(fmt.Errorf("the grant[%s] isn't existed", k)) + return common.NewIgnorableError(fmt.Errorf("the grant[%s] isn't existed", granteeKey)) } return err } if !errors.Is(err, merr.ErrIoKeyNotFound) { return err } - idStr = crypto.MD5(k) - err = kc.Txn.Save(ctx, k, idStr) + idStr = crypto.GranteeID(granteeKey) + err = kc.Txn.Save(ctx, granteeKey, idStr) if err != nil { log.Ctx(ctx).Error("fail to allocate id when altering the grant", zap.Error(err)) return err } } } - k = fmt.Sprintf("%s/%s/%s", GranteeIDPrefix, idStr, privilegeName) + if idStr != crypto.GranteeID(granteeKey) { + idStr, err = kc.migrateGranteeID(ctx, tenant, granteeKey, idStr) + if err != nil { + log.Ctx(ctx).Error("fail to migrate grantee id when altering the grant", zap.String("key", granteeKey), zap.Error(err)) + return err + } + } + k := buildGranteeIDKey(idStr, privilegeName) _, err = kc.Txn.Load(ctx, k) if err != nil { log.Ctx(ctx).Warn("fail to load the grantee id", zap.String("key", k), zap.Error(err)) @@ -1405,14 +1583,13 @@ func (kc *Catalog) ListGrant(ctx context.Context, tenant string, entity *milvusp var entities []*milvuspb.GrantEntity var granteeKey string - appendGrantEntity := func(v string, object string, objectName string) error { + appendGrantEntity := func(granteeKey string, v string, object string, objectName string) error { dbName := "" dbName, objectName = funcutil.SplitObjectName(objectName) if dbName != entity.DbName && dbName != util.AnyWord && entity.DbName != util.AnyWord { return nil } - granteeIDKey := funcutil.HandleTenantForEtcdPrefix(GranteeIDPrefix, tenant, v) - keys, values, err := kc.Txn.LoadWithPrefix(ctx, granteeIDKey) + keys, values, granteeIDKey, err := kc.loadGranteeIDPrefix(ctx, tenant, granteeKey, v) if err != nil { log.Ctx(ctx).Error("fail to load the grantee ids", zap.String("key", granteeIDKey), zap.Error(err)) return err @@ -1446,18 +1623,21 @@ func (kc *Catalog) ListGrant(ctx context.Context, tenant string, entity *milvusp granteeKey = fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, entity.Role.Name, entity.Object.Name, entity.ObjectName) v, err := kc.Txn.Load(ctx, granteeKey) if err == nil { - err = appendGrantEntity(v, entity.Object.Name, entity.ObjectName) + err = appendGrantEntity(granteeKey, v, entity.Object.Name, entity.ObjectName) if err == nil { return entities, nil } + return entities, err } } if entity.DbName != util.AnyWord { granteeKey = fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, entity.Role.Name, entity.Object.Name, funcutil.CombineObjectName(util.AnyWord, entity.ObjectName)) v, err := kc.Txn.Load(ctx, granteeKey) if err == nil { - _ = appendGrantEntity(v, entity.Object.Name, funcutil.CombineObjectName(util.AnyWord, entity.ObjectName)) + if err = appendGrantEntity(granteeKey, v, entity.Object.Name, funcutil.CombineObjectName(util.AnyWord, entity.ObjectName)); err != nil { + return entities, err + } } } @@ -1467,7 +1647,7 @@ func (kc *Catalog) ListGrant(ctx context.Context, tenant string, entity *milvusp log.Ctx(ctx).Error("fail to load the grant privilege entity", zap.String("key", granteeKey), zap.Error(err)) return entities, err } - err = appendGrantEntity(v, entity.Object.Name, funcutil.CombineObjectName(entity.DbName, entity.ObjectName)) + err = appendGrantEntity(granteeKey, v, entity.Object.Name, funcutil.CombineObjectName(entity.DbName, entity.ObjectName)) if err != nil { return entities, err } @@ -1484,7 +1664,8 @@ func (kc *Catalog) ListGrant(ctx context.Context, tenant string, entity *milvusp log.Ctx(ctx).Warn("invalid grantee key", zap.String("string", key), zap.String("sub_string", granteeKey)) continue } - err = appendGrantEntity(values[i], grantInfos[0], grantInfos[1]) + keyWithoutTrailingSlash := strings.TrimSuffix(granteeKey, "/") + err = appendGrantEntity(fmt.Sprintf("%s/%s/%s", keyWithoutTrailingSlash, grantInfos[0], grantInfos[1]), values[i], grantInfos[0], grantInfos[1]) if err != nil { return entities, err } @@ -1504,7 +1685,8 @@ func (kc *Catalog) DeleteGrantByCollectionName(ctx context.Context, tenant strin } var exactRemoveKeys []string - var prefixRemoveKeys []string + var granteeIDs []string + removingGrantees := make(map[string]struct{}) for i, key := range keys { grantInfos := typeutil.AfterN(key, granteeKey, "/") if len(grantInfos) != 3 { @@ -1522,16 +1704,25 @@ func (kc *Catalog) DeleteGrantByCollectionName(ctx context.Context, tenant strin // use the logical key to avoid double-prefix. logicalKey := fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, grantInfos[0], grantInfos[1], grantInfos[2]) exactRemoveKeys = append(exactRemoveKeys, logicalKey) - // Use prefix deletion for the granteeID key (has sub-keys) - granteeIDKey := funcutil.HandleTenantForEtcdPrefix(GranteeIDPrefix, tenant, values[i]) - prefixRemoveKeys = append(prefixRemoveKeys, granteeIDKey) + removingGrantees[logicalKey] = struct{}{} + granteeIDs = append(granteeIDs, values[i]) } } - if len(exactRemoveKeys) == 0 && len(prefixRemoveKeys) == 0 { + if len(exactRemoveKeys) == 0 && len(granteeIDs) == 0 { return nil } + var prefixRemoveKeys []string + for _, idStr := range granteeIDs { + if !shouldRemoveGranteeIDSubtree(ctx, granteeKey, idStr, keys, values, removingGrantees) { + continue + } + // Use prefix deletion for the granteeID key (has sub-keys) + granteeIDKey := funcutil.HandleTenantForEtcdPrefix(GranteeIDPrefix, tenant, idStr) + prefixRemoveKeys = append(prefixRemoveKeys, granteeIDKey) + } + // Use prefix deletion for granteeID keys (which have sub-keys underneath). if len(prefixRemoveKeys) > 0 { if err = kc.Txn.MultiSaveAndRemoveWithPrefix(ctx, nil, prefixRemoveKeys); err != nil { @@ -1577,6 +1768,22 @@ func (kc *Catalog) MigrateGrantCollectionName(ctx context.Context, tenant string grantDB, grantObj := funcutil.SplitObjectName(grantInfos[2]) if grantObj == oldName && grantDB == oldDBName { oldIdStr := values[i] + // Reconstruct logical key (without etcd rootPath) for deletion. + // LoadWithPrefix returns full etcd keys (with rootPath prefix), + // but MultiSaveAndRemove prepends rootPath again, so we must + // use the logical key to avoid double-prefix. + oldKey := fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, grantInfos[0], grantInfos[1], grantInfos[2]) + if isLegacyGranteeID(oldIdStr) { + otherGranteeKey, err := findOtherGranteeWithIDFromKeys(ctx, granteeKey, oldKey, oldIdStr, keys, values) + if err != nil { + removeKeys = append(removeKeys, oldKey) + continue + } + if otherGranteeKey != "" { + removeKeys = append(removeKeys, oldKey) + continue + } + } // Load GranteeIDPrefix entries FIRST, before queuing the parent key // for migration. If this load fails, we skip both parent and child @@ -1594,13 +1801,8 @@ func (kc *Catalog) MigrateGrantCollectionName(ctx context.Context, tenant string // that reuses the old name. newObjName := funcutil.CombineObjectName(newDBName, newName) newKey := fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, grantInfos[0], grantInfos[1], newObjName) - newIdStr := crypto.MD5(newKey) + newIdStr := crypto.GranteeID(newKey) saves[newKey] = newIdStr - // Reconstruct logical key (without etcd rootPath) for deletion. - // LoadWithPrefix returns full etcd keys (with rootPath prefix), - // but MultiSaveAndRemove prepends rootPath again, so we must - // use the logical key to avoid double-prefix. - oldKey := fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, grantInfos[0], grantInfos[1], grantInfos[2]) removeKeys = append(removeKeys, oldKey) // Migrate GranteeIDPrefix entries from oldIdStr to newIdStr @@ -1646,12 +1848,36 @@ func (kc *Catalog) DeleteGrant(ctx context.Context, tenant string, role *milvusp removeKeys = append(removeKeys, k) // the values are the grantee id list - _, values, err := kc.Txn.LoadWithPrefix(ctx, k) + keys, values, err := kc.Txn.LoadWithPrefix(ctx, k) if err != nil { log.Ctx(ctx).Warn("fail to load grant privilege entities", zap.String("key", k), zap.Error(err)) return err } + removingGrantees := make(map[string]struct{}) + keyWithoutTrailingSlash := strings.TrimSuffix(k, "/") + for _, key := range keys { + grantInfos := typeutil.AfterN(key, k, "/") + if len(grantInfos) != 2 { + log.Ctx(ctx).Warn("invalid grantee key while deleting role", + zap.String("key", key), zap.String("prefix", k)) + continue + } + removingGrantees[fmt.Sprintf("%s/%s/%s", keyWithoutTrailingSlash, grantInfos[0], grantInfos[1])] = struct{}{} + } + var allKeys []string + var allValues []string for _, v := range values { + if isLegacyGranteeID(v) && allKeys == nil { + granteePrefix := funcutil.HandleTenantForEtcdPrefix(GranteePrefix, tenant) + allKeys, allValues, err = kc.Txn.LoadWithPrefix(ctx, granteePrefix) + if err != nil { + log.Ctx(ctx).Warn("fail to load grant privilege entities for shared id check", zap.String("key", granteePrefix), zap.Error(err)) + return err + } + } + if !shouldRemoveGranteeIDSubtree(ctx, funcutil.HandleTenantForEtcdPrefix(GranteePrefix, tenant), v, allKeys, allValues, removingGrantees) { + continue + } granteeIDKey := funcutil.HandleTenantForEtcdPrefix(GranteeIDPrefix, tenant, v) removeKeys = append(removeKeys, granteeIDKey) } @@ -1677,8 +1903,8 @@ func (kc *Catalog) ListPolicy(ctx context.Context, tenant string) ([]*milvuspb.G log.Ctx(ctx).Warn("invalid grantee key", zap.String("string", key), zap.String("sub_string", granteeKey)) continue } - granteeIDKey := funcutil.HandleTenantForEtcdPrefix(GranteeIDPrefix, tenant, values[i]) - idKeys, _, err := kc.Txn.LoadWithPrefix(ctx, granteeIDKey) + logicalGranteeKey := fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, grantInfos[0], grantInfos[1], grantInfos[2]) + idKeys, _, granteeIDKey, err := kc.loadGranteeIDPrefix(ctx, tenant, logicalGranteeKey, values[i]) if err != nil { log.Ctx(ctx).Error("fail to load the grantee ids", zap.String("key", granteeIDKey), zap.Error(err)) return []*milvuspb.GrantEntity{}, err
internal/metastore/kv/rootcoord/kv_catalog_test.go+432 −6 modified@@ -2282,14 +2282,14 @@ func TestRBAC_Grant(t *testing.T) { ) validRoleKey := fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, validRole, object, objName) - validRoleValue := crypto.MD5(validRoleKey) + validRoleValue := crypto.GranteeID(validRoleKey) invalidRoleKey := fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, invalidRole, object, objName) invalidRoleKeyWithDb := fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, invalidRole, object, funcutil.CombineObjectName(util.DefaultDBName, objName)) keyNotExistRoleKey := fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, keyNotExistRole, object, objName) keyNotExistRoleKeyWithDb := fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, keyNotExistRole, object, funcutil.CombineObjectName(util.DefaultDBName, objName)) - keyNotExistRoleValueWithDb := crypto.MD5(keyNotExistRoleKeyWithDb) + keyNotExistRoleValueWithDb := crypto.GranteeID(keyNotExistRoleKeyWithDb) errorSaveRoleKey := fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, errorSaveRole, object, objName) errorSaveRoleKeyWithDb := fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, errorSaveRole, object, funcutil.CombineObjectName(util.DefaultDBName, objName)) @@ -2733,6 +2733,385 @@ func TestRBAC_Grant(t *testing.T) { }) } +func TestRBACGrantLegacyGranteeIDCompatibility(t *testing.T) { + ctx := context.Background() + etcdCli, _ := etcd.GetEtcdClient( + Params.EtcdCfg.UseEmbedEtcd.GetAsBool(), + Params.EtcdCfg.EtcdUseSSL.GetAsBool(), + Params.EtcdCfg.Endpoints.GetAsStrings(), + Params.EtcdCfg.EtcdTLSCert.GetValue(), + Params.EtcdCfg.EtcdTLSKey.GetValue(), + Params.EtcdCfg.EtcdTLSCACert.GetValue(), + Params.EtcdCfg.EtcdTLSMinVersion.GetValue()) + rootPath := fmt.Sprintf("/test/rbac/legacy-grantee-id-%d", rand.Int()) + metaKV := etcdkv.NewEtcdKV(etcdCli, rootPath) + defer metaKV.RemoveWithPrefix(ctx, "") + defer metaKV.Close() + c := NewCatalog(metaKV) + + roleName := "legacy-role" + objectName := "legacy-collection" + granteeKey := fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, roleName, commonpb.ObjectType_Collection.String(), funcutil.CombineObjectName(util.DefaultDBName, objectName)) + legacyID := crypto.MD5(granteeKey) + legacyPrivilegeKey := fmt.Sprintf("%s/%s/%s", GranteeIDPrefix, legacyID, "PrivilegeLoad") + + require.Len(t, legacyID, 16) + require.NoError(t, metaKV.Save(ctx, granteeKey, legacyID)) + require.NoError(t, metaKV.Save(ctx, legacyPrivilegeKey, "legacy-user")) + + assertPolicyPrivileges := func(expected []string) { + policies, err := c.ListPolicy(ctx, util.DefaultTenant) + require.NoError(t, err) + require.Len(t, policies, len(expected)) + assert.ElementsMatch(t, expected, lo.Map(policies, func(policy *milvuspb.GrantEntity, _ int) string { + assert.Equal(t, roleName, policy.GetRole().GetName()) + assert.Equal(t, commonpb.ObjectType_Collection.String(), policy.GetObject().GetName()) + assert.Equal(t, objectName, policy.GetObjectName()) + assert.Equal(t, util.DefaultDBName, policy.GetDbName()) + return policy.GetGrantor().GetPrivilege().GetName() + })) + } + assertPolicyPrivileges([]string{"Load"}) + + listEntity := &milvuspb.GrantEntity{ + Role: &milvuspb.RoleEntity{Name: roleName}, + Object: &milvuspb.ObjectEntity{Name: commonpb.ObjectType_Collection.String()}, + ObjectName: objectName, + DbName: util.DefaultDBName, + } + grants, err := c.ListGrant(ctx, util.DefaultTenant, listEntity) + require.NoError(t, err) + require.Len(t, grants, 1) + assert.Equal(t, "legacy-user", grants[0].GetGrantor().GetUser().GetName()) + assert.Equal(t, "Load", grants[0].GetGrantor().GetPrivilege().GetName()) + + releaseGrant := &milvuspb.GrantEntity{ + Role: &milvuspb.RoleEntity{Name: roleName}, + Object: &milvuspb.ObjectEntity{Name: commonpb.ObjectType_Collection.String()}, + ObjectName: objectName, + DbName: util.DefaultDBName, + Grantor: &milvuspb.GrantorEntity{ + User: &milvuspb.UserEntity{Name: "new-user"}, + Privilege: &milvuspb.PrivilegeEntity{Name: "PrivilegeRelease"}, + }, + } + require.NoError(t, c.AlterGrant(ctx, util.DefaultTenant, releaseGrant, milvuspb.OperatePrivilegeType_Grant)) + + newID := crypto.GranteeID(granteeKey) + require.Len(t, newID, 32) + storedID, err := metaKV.Load(ctx, granteeKey) + require.NoError(t, err) + assert.Equal(t, newID, storedID) + + grants, err = c.ListGrant(ctx, util.DefaultTenant, listEntity) + require.NoError(t, err) + require.Len(t, grants, 2) + assert.ElementsMatch(t, []string{"Load", "Release"}, lo.Map(grants, func(grant *milvuspb.GrantEntity, _ int) string { + return grant.GetGrantor().GetPrivilege().GetName() + })) + assertPolicyPrivileges([]string{"Load", "Release"}) + legacyKeys, _, err := metaKV.LoadWithPrefix(ctx, funcutil.HandleTenantForEtcdPrefix(GranteeIDPrefix, util.DefaultTenant, legacyID)) + require.NoError(t, err) + assert.Empty(t, legacyKeys) + + revokeGrant := &milvuspb.GrantEntity{ + Role: &milvuspb.RoleEntity{Name: roleName}, + Object: &milvuspb.ObjectEntity{Name: commonpb.ObjectType_Collection.String()}, + ObjectName: objectName, + DbName: util.DefaultDBName, + Grantor: &milvuspb.GrantorEntity{ + User: &milvuspb.UserEntity{Name: "legacy-user"}, + Privilege: &milvuspb.PrivilegeEntity{Name: "PrivilegeLoad"}, + }, + } + require.NoError(t, c.AlterGrant(ctx, util.DefaultTenant, revokeGrant, milvuspb.OperatePrivilegeType_Revoke)) + + grants, err = c.ListGrant(ctx, util.DefaultTenant, listEntity) + require.NoError(t, err) + require.Len(t, grants, 1) + assert.Equal(t, "Release", grants[0].GetGrantor().GetPrivilege().GetName()) + assertPolicyPrivileges([]string{"Release"}) +} + +func TestRBACGrantSharedLegacyGranteeIDMigrationFailsClosed(t *testing.T) { + ctx := context.Background() + etcdCli, _ := etcd.GetEtcdClient( + Params.EtcdCfg.UseEmbedEtcd.GetAsBool(), + Params.EtcdCfg.EtcdUseSSL.GetAsBool(), + Params.EtcdCfg.Endpoints.GetAsStrings(), + Params.EtcdCfg.EtcdTLSCert.GetValue(), + Params.EtcdCfg.EtcdTLSKey.GetValue(), + Params.EtcdCfg.EtcdTLSCACert.GetValue(), + Params.EtcdCfg.EtcdTLSMinVersion.GetValue()) + rootPath := fmt.Sprintf("/test/rbac/shared-legacy-grantee-id-%d", rand.Int()) + metaKV := etcdkv.NewEtcdKV(etcdCli, rootPath) + defer metaKV.RemoveWithPrefix(ctx, "") + defer metaKV.Close() + c := NewCatalog(metaKV) + + objectName := "shared-legacy-collection" + objectType := commonpb.ObjectType_Collection.String() + donorKey := fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, "donor-role", objectType, funcutil.CombineObjectName(util.DefaultDBName, objectName)) + victimKey := fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, "victim-role", objectType, funcutil.CombineObjectName(util.DefaultDBName, objectName)) + sharedLegacyID := crypto.MD5(donorKey) + donorPrivilegeKey := buildGranteeIDKey(sharedLegacyID, "PrivilegeInsert") + + require.NoError(t, metaKV.MultiSave(ctx, map[string]string{ + donorKey: sharedLegacyID, + victimKey: sharedLegacyID, + donorPrivilegeKey: "donor-user", + })) + + victimListEntity := &milvuspb.GrantEntity{ + Role: &milvuspb.RoleEntity{Name: "victim-role"}, + Object: &milvuspb.ObjectEntity{Name: objectType}, + ObjectName: objectName, + DbName: util.DefaultDBName, + } + victimGrants, err := c.ListGrant(ctx, util.DefaultTenant, victimListEntity) + require.Error(t, err) + assert.Contains(t, err.Error(), "shared legacy grantee id") + assert.Empty(t, victimGrants) + policies, err := c.ListPolicy(ctx, util.DefaultTenant) + require.Error(t, err) + assert.Contains(t, err.Error(), "shared legacy grantee id") + assert.Empty(t, policies) + + err = c.AlterGrant(ctx, util.DefaultTenant, &milvuspb.GrantEntity{ + Role: &milvuspb.RoleEntity{Name: "victim-role"}, + Object: &milvuspb.ObjectEntity{Name: objectType}, + ObjectName: objectName, + DbName: util.DefaultDBName, + Grantor: &milvuspb.GrantorEntity{ + User: &milvuspb.UserEntity{Name: "victim-user"}, + Privilege: &milvuspb.PrivilegeEntity{Name: "PrivilegeLoad"}, + }, + }, milvuspb.OperatePrivilegeType_Grant) + require.Error(t, err) + assert.Contains(t, err.Error(), "shared legacy grantee id") + + storedID, err := metaKV.Load(ctx, victimKey) + require.NoError(t, err) + assert.Equal(t, sharedLegacyID, storedID) + + victimFullID := crypto.GranteeID(victimKey) + victimFullIDKeys, _, err := metaKV.LoadWithPrefix(ctx, funcutil.HandleTenantForEtcdPrefix(GranteeIDPrefix, util.DefaultTenant, victimFullID)) + require.NoError(t, err) + assert.Empty(t, victimFullIDKeys) + + donorUser, err := metaKV.Load(ctx, donorPrivilegeKey) + require.NoError(t, err) + assert.Equal(t, "donor-user", donorUser) +} + +func TestRBACGrantMigrationIgnoresUnreferencedComputedLegacyID(t *testing.T) { + ctx := context.Background() + etcdCli, _ := etcd.GetEtcdClient( + Params.EtcdCfg.UseEmbedEtcd.GetAsBool(), + Params.EtcdCfg.EtcdUseSSL.GetAsBool(), + Params.EtcdCfg.Endpoints.GetAsStrings(), + Params.EtcdCfg.EtcdTLSCert.GetValue(), + Params.EtcdCfg.EtcdTLSKey.GetValue(), + Params.EtcdCfg.EtcdTLSCACert.GetValue(), + Params.EtcdCfg.EtcdTLSMinVersion.GetValue()) + rootPath := fmt.Sprintf("/test/rbac/unreferenced-computed-legacy-grantee-id-%d", rand.Int()) + metaKV := etcdkv.NewEtcdKV(etcdCli, rootPath) + defer metaKV.RemoveWithPrefix(ctx, "") + defer metaKV.Close() + c := NewCatalog(metaKV) + + roleName := "custom-id-role" + objectName := "custom-id-collection" + objectType := commonpb.ObjectType_Collection.String() + granteeKey := fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, roleName, objectType, funcutil.CombineObjectName(util.DefaultDBName, objectName)) + storedLegacyID := "0123456789abcdef" + computedLegacyID := crypto.MD5(granteeKey) + require.NotEqual(t, storedLegacyID, computedLegacyID) + + require.NoError(t, metaKV.MultiSave(ctx, map[string]string{ + granteeKey: storedLegacyID, + buildGranteeIDKey(computedLegacyID, "PrivilegeInsert"): "donor-user", + })) + + listEntity := &milvuspb.GrantEntity{ + Role: &milvuspb.RoleEntity{Name: roleName}, + Object: &milvuspb.ObjectEntity{Name: objectType}, + ObjectName: objectName, + DbName: util.DefaultDBName, + } + grants, err := c.ListGrant(ctx, util.DefaultTenant, listEntity) + require.NoError(t, err) + assert.Empty(t, grants) + + require.NoError(t, c.AlterGrant(ctx, util.DefaultTenant, &milvuspb.GrantEntity{ + Role: &milvuspb.RoleEntity{Name: roleName}, + Object: &milvuspb.ObjectEntity{Name: objectType}, + ObjectName: objectName, + DbName: util.DefaultDBName, + Grantor: &milvuspb.GrantorEntity{ + User: &milvuspb.UserEntity{Name: "victim-user"}, + Privilege: &milvuspb.PrivilegeEntity{Name: "PrivilegeLoad"}, + }, + }, milvuspb.OperatePrivilegeType_Grant)) + + newID := crypto.GranteeID(granteeKey) + fullIDKeys, _, err := metaKV.LoadWithPrefix(ctx, funcutil.HandleTenantForEtcdPrefix(GranteeIDPrefix, util.DefaultTenant, newID)) + require.NoError(t, err) + assert.ElementsMatch(t, []string{fmt.Sprintf("%s/%s/%s", rootPath, GranteeIDPrefix, newID) + "/PrivilegeLoad"}, fullIDKeys) + + orphanUser, err := metaKV.Load(ctx, buildGranteeIDKey(computedLegacyID, "PrivilegeInsert")) + require.NoError(t, err) + assert.Equal(t, "donor-user", orphanUser) +} + +func TestRBACGrantDeleteSharedLegacyGranteeIDKeepsSurvivorSubtree(t *testing.T) { + ctx := context.Background() + etcdCli, _ := etcd.GetEtcdClient( + Params.EtcdCfg.UseEmbedEtcd.GetAsBool(), + Params.EtcdCfg.EtcdUseSSL.GetAsBool(), + Params.EtcdCfg.Endpoints.GetAsStrings(), + Params.EtcdCfg.EtcdTLSCert.GetValue(), + Params.EtcdCfg.EtcdTLSKey.GetValue(), + Params.EtcdCfg.EtcdTLSCACert.GetValue(), + Params.EtcdCfg.EtcdTLSMinVersion.GetValue()) + rootPath := fmt.Sprintf("/test/rbac/delete-shared-legacy-grantee-id-%d", rand.Int()) + metaKV := etcdkv.NewEtcdKV(etcdCli, rootPath) + defer metaKV.RemoveWithPrefix(ctx, "") + defer metaKV.Close() + c := NewCatalog(metaKV) + + objectType := commonpb.ObjectType_Collection.String() + victimRole := "victim-role" + survivorRole := "survivor-role" + victimKey := fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, victimRole, objectType, funcutil.CombineObjectName(util.DefaultDBName, "victim-col")) + survivorKey := fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, survivorRole, objectType, funcutil.CombineObjectName(util.DefaultDBName, "survivor-col")) + sharedLegacyID := crypto.MD5(victimKey) + sharedPrivilegeKey := buildGranteeIDKey(sharedLegacyID, "PrivilegeInsert") + require.NoError(t, metaKV.MultiSave(ctx, map[string]string{ + victimKey: sharedLegacyID, + survivorKey: sharedLegacyID, + sharedPrivilegeKey: "shared-user", + })) + + require.NoError(t, c.DeleteGrant(ctx, util.DefaultTenant, &milvuspb.RoleEntity{Name: victimRole})) + + _, err := metaKV.Load(ctx, victimKey) + require.Error(t, err) + survivorID, err := metaKV.Load(ctx, survivorKey) + require.NoError(t, err) + assert.Equal(t, sharedLegacyID, survivorID) + sharedUser, err := metaKV.Load(ctx, sharedPrivilegeKey) + require.NoError(t, err) + assert.Equal(t, "shared-user", sharedUser) +} + +func TestRBACGrantDeleteCollectionSharedLegacyGranteeIDKeepsSurvivorSubtree(t *testing.T) { + ctx := context.Background() + etcdCli, _ := etcd.GetEtcdClient( + Params.EtcdCfg.UseEmbedEtcd.GetAsBool(), + Params.EtcdCfg.EtcdUseSSL.GetAsBool(), + Params.EtcdCfg.Endpoints.GetAsStrings(), + Params.EtcdCfg.EtcdTLSCert.GetValue(), + Params.EtcdCfg.EtcdTLSKey.GetValue(), + Params.EtcdCfg.EtcdTLSCACert.GetValue(), + Params.EtcdCfg.EtcdTLSMinVersion.GetValue()) + rootPath := fmt.Sprintf("/test/rbac/delete-collection-shared-legacy-grantee-id-%d", rand.Int()) + metaKV := etcdkv.NewEtcdKV(etcdCli, rootPath) + defer metaKV.RemoveWithPrefix(ctx, "") + defer metaKV.Close() + c := NewCatalog(metaKV) + + objectType := commonpb.ObjectType_Collection.String() + droppedKey := fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, "dropped-role", objectType, funcutil.CombineObjectName(util.DefaultDBName, "dropped-col")) + survivorKey := fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, "survivor-role", objectType, funcutil.CombineObjectName(util.DefaultDBName, "survivor-col")) + sharedLegacyID := crypto.MD5(droppedKey) + sharedPrivilegeKey := buildGranteeIDKey(sharedLegacyID, "PrivilegeInsert") + require.NoError(t, metaKV.MultiSave(ctx, map[string]string{ + droppedKey: sharedLegacyID, + survivorKey: sharedLegacyID, + sharedPrivilegeKey: "shared-user", + })) + + require.NoError(t, c.DeleteGrantByCollectionName(ctx, util.DefaultTenant, util.DefaultDBName, "dropped-col")) + + _, err := metaKV.Load(ctx, droppedKey) + require.Error(t, err) + survivorID, err := metaKV.Load(ctx, survivorKey) + require.NoError(t, err) + assert.Equal(t, sharedLegacyID, survivorID) + sharedUser, err := metaKV.Load(ctx, sharedPrivilegeKey) + require.NoError(t, err) + assert.Equal(t, "shared-user", sharedUser) +} + +func TestRBACGrantSharedLegacyGranteeIDListGrantFallbacksFailClosed(t *testing.T) { + ctx := context.Background() + etcdCli, _ := etcd.GetEtcdClient( + Params.EtcdCfg.UseEmbedEtcd.GetAsBool(), + Params.EtcdCfg.EtcdUseSSL.GetAsBool(), + Params.EtcdCfg.Endpoints.GetAsStrings(), + Params.EtcdCfg.EtcdTLSCert.GetValue(), + Params.EtcdCfg.EtcdTLSKey.GetValue(), + Params.EtcdCfg.EtcdTLSCACert.GetValue(), + Params.EtcdCfg.EtcdTLSMinVersion.GetValue()) + + testCases := []struct { + name string + unsafeDB string + }{ + { + name: "legacy no db key", + unsafeDB: "", + }, + { + name: "wildcard db key", + unsafeDB: util.AnyWord, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + objectName := strings.ReplaceAll(test.name, " ", "-") + rootPath := fmt.Sprintf("/test/rbac/shared-legacy-fallback-%s-%d", objectName, rand.Int()) + metaKV := etcdkv.NewEtcdKV(etcdCli, rootPath) + defer metaKV.RemoveWithPrefix(ctx, "") + defer metaKV.Close() + c := NewCatalog(metaKV) + + roleName := "victim-role" + objectType := commonpb.ObjectType_Collection.String() + donorKey := fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, "donor-role", objectType, funcutil.CombineObjectName(util.DefaultDBName, objectName)) + sharedLegacyID := crypto.MD5(donorKey) + var unsafeKey string + if test.unsafeDB == "" { + unsafeKey = fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, roleName, objectType, objectName) + } else { + unsafeKey = fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, roleName, objectType, funcutil.CombineObjectName(test.unsafeDB, objectName)) + } + exactKey := fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, roleName, objectType, funcutil.CombineObjectName(util.DefaultDBName, objectName)) + exactID := crypto.GranteeID(exactKey) + + require.NoError(t, metaKV.MultiSave(ctx, map[string]string{ + donorKey: sharedLegacyID, + unsafeKey: sharedLegacyID, + buildGranteeIDKey(sharedLegacyID, "PrivilegeInsert"): "donor-user", + exactKey: exactID, + buildGranteeIDKey(exactID, "PrivilegeLoad"): "victim-user", + })) + + grants, err := c.ListGrant(ctx, util.DefaultTenant, &milvuspb.GrantEntity{ + Role: &milvuspb.RoleEntity{Name: roleName}, + Object: &milvuspb.ObjectEntity{Name: objectType}, + ObjectName: objectName, + DbName: util.DefaultDBName, + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "shared legacy grantee id") + assert.Empty(t, grants) + }) + } +} + func TestRBAC_Backup(t *testing.T) { etcdCli, _ := etcd.GetEtcdClient( Params.EtcdCfg.UseEmbedEtcd.GetAsBool(), @@ -3753,8 +4132,8 @@ func TestMigrateGrantCollectionName(t *testing.T) { "role1", "Collection", funcutil.CombineObjectName("default", "new_col")) newKey2 := fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, "role2", "Collection", funcutil.CombineObjectName("default", "new_col")) - newIdStr1 := crypto.MD5(newKey1) - newIdStr2 := crypto.MD5(newKey2) + newIdStr1 := crypto.GranteeID(newKey1) + newIdStr2 := crypto.GranteeID(newKey2) // Mock loading GranteeIDPrefix entries for each old idStr oldGranteeIDKey1 := funcutil.HandleTenantForEtcdPrefix(GranteeIDPrefix, tenant, "gid1") @@ -3795,7 +4174,7 @@ func TestMigrateGrantCollectionName(t *testing.T) { newKey1 := fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, "role1", "Collection", funcutil.CombineObjectName("db2", "col2")) - newIdStr1 := crypto.MD5(newKey1) + newIdStr1 := crypto.GranteeID(newKey1) // Mock loading GranteeIDPrefix entries for old idStr oldGranteeIDKey1 := funcutil.HandleTenantForEtcdPrefix(GranteeIDPrefix, tenant, "gid1") @@ -3829,7 +4208,7 @@ func TestMigrateGrantCollectionName(t *testing.T) { newKey2 := fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, "role1", "Collection", funcutil.CombineObjectName("default", "new_col")) - newIdStr2 := crypto.MD5(newKey2) + newIdStr2 := crypto.GranteeID(newKey2) kvmock.EXPECT().MultiSaveAndRemove(mock.Anything, map[string]string{newKey2: newIdStr2}, @@ -3839,6 +4218,53 @@ func TestMigrateGrantCollectionName(t *testing.T) { assert.NoError(t, err) }) + t.Run("shared legacy grantee id removes old parent without copying privileges", func(t *testing.T) { + etcdCli, _ := etcd.GetEtcdClient( + Params.EtcdCfg.UseEmbedEtcd.GetAsBool(), + Params.EtcdCfg.EtcdUseSSL.GetAsBool(), + Params.EtcdCfg.Endpoints.GetAsStrings(), + Params.EtcdCfg.EtcdTLSCert.GetValue(), + Params.EtcdCfg.EtcdTLSKey.GetValue(), + Params.EtcdCfg.EtcdTLSCACert.GetValue(), + Params.EtcdCfg.EtcdTLSMinVersion.GetValue()) + rootPath := fmt.Sprintf("/test/rbac/rename-shared-legacy-grantee-id-%d", rand.Int()) + metaKV := etcdkv.NewEtcdKV(etcdCli, rootPath) + defer metaKV.RemoveWithPrefix(ctx, "") + defer metaKV.Close() + c := NewCatalog(metaKV) + + donorKey := fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, "donor-role", "Collection", funcutil.CombineObjectName("default", "old_col")) + victimKey := fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, "victim-role", "Collection", funcutil.CombineObjectName("default", "old_col")) + sharedLegacyID := crypto.MD5(donorKey) + require.NoError(t, metaKV.MultiSave(ctx, map[string]string{ + donorKey: sharedLegacyID, + victimKey: sharedLegacyID, + buildGranteeIDKey(sharedLegacyID, "PrivilegeInsert"): "donor-user", + })) + + err := c.MigrateGrantCollectionName(ctx, tenant, "default", "old_col", "default", "new_col") + require.NoError(t, err) + + donorNewKey := fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, "donor-role", "Collection", funcutil.CombineObjectName("default", "new_col")) + donorNewID := crypto.GranteeID(donorNewKey) + victimNewKey := fmt.Sprintf("%s/%s/%s/%s", GranteePrefix, "victim-role", "Collection", funcutil.CombineObjectName("default", "new_col")) + victimNewID := crypto.GranteeID(victimNewKey) + donorFullIDKeys, _, err := metaKV.LoadWithPrefix(ctx, funcutil.HandleTenantForEtcdPrefix(GranteeIDPrefix, tenant, donorNewID)) + require.NoError(t, err) + assert.Empty(t, donorFullIDKeys) + victimFullIDKeys, _, err := metaKV.LoadWithPrefix(ctx, funcutil.HandleTenantForEtcdPrefix(GranteeIDPrefix, tenant, victimNewID)) + require.NoError(t, err) + assert.Empty(t, victimFullIDKeys) + + _, err = metaKV.Load(ctx, donorKey) + require.Error(t, err) + _, err = metaKV.Load(ctx, victimKey) + require.Error(t, err) + sharedUser, err := metaKV.Load(ctx, buildGranteeIDKey(sharedLegacyID, "PrivilegeInsert")) + require.NoError(t, err) + assert.Equal(t, "donor-user", sharedUser) + }) + t.Run("save error", func(t *testing.T) { kvmock := mocks.NewTxnKV(t) c := NewCatalog(kvmock)
pkg/util/crypto/crypto.go+6 −0 modified@@ -46,3 +46,9 @@ func MD5(str string) string { data := md5.Sum([]byte(str)) return hex.EncodeToString(data[:])[8:24] } + +func GranteeID(str string) string { + // #nosec G401 -- RBAC grantee IDs need stable 128-bit identifiers, not password hashing. + data := md5.Sum([]byte(str)) + return hex.EncodeToString(data[:]) +}
pkg/util/crypto/crypto_test.go+28 −0 modified@@ -1,9 +1,12 @@ package crypto import ( + "encoding/hex" + "fmt" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "golang.org/x/crypto/bcrypt" ) @@ -45,3 +48,28 @@ func TestBcryptCost(t *testing.T) { func TestMD5(t *testing.T) { assert.Equal(t, "67f48520697662a2", MD5("These pretzels are making me thirsty.")) } + +func TestGranteeID(t *testing.T) { + id := GranteeID("These pretzels are making me thirsty.") + require.Len(t, id, 32) + _, err := hex.DecodeString(id) + require.NoError(t, err) + assert.Equal(t, "b0804ec967f48520697662a204f5fe72", id) +} + +func TestGranteeIDCollisionResistance(t *testing.T) { + const grantCount = 1024 + seen := make(map[string]string, grantCount) + + for i := 0; i < grantCount; i++ { + key := fmt.Sprintf("root-coord/credential/grantee-privileges/role-%d/Collection/default.collection-%d", i, i) + id := GranteeID(key) + require.Len(t, id, 32) + + fullMD5Prefix := id[:32] + if previousKey, ok := seen[fullMD5Prefix]; ok { + t.Fatalf("grantee ID collision for %q and %q: %s", previousKey, key, fullMD5Prefix) + } + seen[fullMD5Prefix] = key + } +}
Vulnerability mechanics
Root cause
"The Grantee ID Hash Handler uses a weak hashing algorithm, making it susceptible to collisions."
Attack vector
An attacker with local access must exploit a high-complexity attack to trigger this vulnerability. The difficulty of exploitability is rated as high, suggesting significant effort is required to successfully leverage the weak hash. The public disclosure of this vulnerability means it may be actively exploited.
Affected code
The vulnerability resides within the `internal/metastore/kv/rootcoord/kv_catalog.go` file, specifically in the Grantee ID Hash Handler component.
What the fix does
The patch addresses the use of a weak hash in the Grantee ID Hash Handler. By implementing a more robust hashing mechanism, the patch mitigates the risk of hash collisions, thereby resolving the vulnerability. The specific details of the hashing algorithm change are not provided in the bundle, but the patch aims to strengthen the security of ID handling.
Preconditions
- inputLocal access is required to perform the attack.
Generated on Jun 4, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7News mentions
0No linked articles in our index yet.