Privilege escalation / Authorization bypass via wildcard resource name in RBAC resourcepermissions

HIGH
grafana/grafana
Commit: 1744b6fc9aff
Affected: <= 12.3.x (prior to this fix in the 12.4.0 line)
2026-04-13 17:06 UTC

Description

The commit adds validation to reject wildcard '*' as the resource name in ResourcePermission objects used by Grafana's RBAC. This was enabling a potential authorization bypass/privilege escalation by creating or using a permission that targets all resources within a group (a wildcard). The changes enforce that resource permissions must target a specific resource and adjust related parsing/validation paths accordingly. Tests were added to ensure '*' is rejected, and an explicit error path was introduced for wildcard resource IDs. This indicates a real vulnerability fix rather than a mere dependency bump or cleanup.

Proof of Concept

PoC (exploitable on versions affected by the vulnerability): Prereqs: Grafana with RBAC enabled, ResourcePermissions CRDs in place, and an attacker able to create or modify ResourcePermission entries or otherwise influence RBAC grants. 1) Before the fix, an attacker could create a ResourcePermission with Name set to '*' for a specific Resource (e.g., folders) within a group/api: { "spec": { "resource": { "apiGroup": "folder.grafana.app", "resource": "folders", "name": "*" // wildcard resource name }, "permissions": [ { "kind": "BasicRole", "name": "Editor", "verb": "edit" } ] } } 2) The wildcard would be treated as a match-all for the resource type, potentially granting edits on all folders to the attacker, bypassing fine-grained access controls. 3) Exploit steps (pre-fix behavior): - Authenticate as a user with limited access. - Create or apply a ResourcePermission with resource.name = '*'. - Attempt to read/edit a resource that should be disallowed (e.g., a restricted folder named 'secret'). - Observe that the operation succeeds due to wildcard matching. 4) After the fix, attempts to create a ResourcePermission with name '*' will be rejected with a BadRequest error, preventing the wildcard-based privilege escalation. Curl example (illustrative, pre-fix could succeed): curl -X POST https://grafana.example/api/iam/resourcepermissions \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{"spec": {"resource": {"apiGroup": "folder.grafana.app", "resource": "folders", "name": "*"}, "permissions": [{"kind": "BasicRole", "name": "Editor", "verb": "edit"}]}}' Expected result before fix: HTTP 200 and permission granted across all folders. Expected result after fix: HTTP 400 Bad Request due to wildcard resource name being invalid.

Commit Details

Author: Gabriel MABILLE

Date: 2026-04-13 12:39 UTC

Message:

RBAC: Reject `*` resource name in resource permissions writes (#122425) * RBAC: Validate `*` resource permissions * lint

Triage Assessment

Vulnerability Type: Privilege Escalation / Authorization Ballback

Confidence: HIGH

Reasoning:

The commit adds validation to reject wildcard '*' as a resource name in resource permissions, preventing broad or unrestricted permission scopes. It enforces that resource permissions must target a specific resource, addressing potential authorization bypass via wildcard naming. Code changes also guard against '*' in related identifiers and error handling paths.

Verification Assessment

Vulnerability Type: Privilege escalation / Authorization bypass via wildcard resource name in RBAC resourcepermissions

Confidence: HIGH

Affected Versions: <= 12.3.x (prior to this fix in the 12.4.0 line)

Code Diff

diff --git a/pkg/registry/apis/iam/resourcepermission/mapper.go b/pkg/registry/apis/iam/resourcepermission/mapper.go index 75782fa323a07..e4faf2300b786 100644 --- a/pkg/registry/apis/iam/resourcepermission/mapper.go +++ b/pkg/registry/apis/iam/resourcepermission/mapper.go @@ -186,10 +186,7 @@ func (m *MappersRegistry) ParseScope(scope, datasourceType string) (*groupResour return nil, fmt.Errorf("%w: %s", errUnknownGroupResource, parts[0]) } - group := gr.Group - if parts[2] != "*" || datasourceType != "" { - group = resolveGroup(group, datasourceType) - } + group := resolveGroup(gr.Group, datasourceType) return &groupResourceName{Group: group, Resource: gr.Resource, Name: parts[2]}, nil } @@ -286,10 +283,7 @@ func (m *MappersRegistry) ParseScopeCtx(ctx context.Context, ns types.NamespaceI } entry := m.entries[gr] - group := gr.Group - if parts[2] != "*" || datasourceType != "" { - group = resolveGroup(group, datasourceType) - } + group := resolveGroup(gr.Group, datasourceType) name := parts[2] if isIDScoped(entry.mapper) && store != nil { diff --git a/pkg/registry/apis/iam/resourcepermission/mapper_test.go b/pkg/registry/apis/iam/resourcepermission/mapper_test.go index 4e11d21fe9532..5bae23a48682e 100644 --- a/pkg/registry/apis/iam/resourcepermission/mapper_test.go +++ b/pkg/registry/apis/iam/resourcepermission/mapper_test.go @@ -325,21 +325,6 @@ func TestMappersRegistry_Wildcard_ParseScope(t *testing.T) { assert.Equal(t, "loki.datasource.grafana.app", grn.Group) assert.Equal(t, "datasources", grn.Resource) assert.Equal(t, "ds1", grn.Name) - - t.Run("wildcard scope identifier preserves wildcard group", func(t *testing.T) { - grn, err := m.ParseScope("datasources:uid:*", "") - require.NoError(t, err) - assert.Equal(t, "*.datasource.grafana.app", grn.Group) - assert.Equal(t, "datasources", grn.Resource) - assert.Equal(t, "*", grn.Name) - }) - - t.Run("wildcard scope identifier resolves group when type is provided", func(t *testing.T) { - grn, err := m.ParseScope("datasources:uid:*", "loki") - require.NoError(t, err) - assert.Equal(t, "loki.datasource.grafana.app", grn.Group) - assert.Equal(t, "*", grn.Name) - }) } func TestMappersRegistry_MultipleWildcards(t *testing.T) { diff --git a/pkg/registry/apis/iam/resourcepermission/validate.go b/pkg/registry/apis/iam/resourcepermission/validate.go index 906f00e86db04..299adb5b3bc6b 100644 --- a/pkg/registry/apis/iam/resourcepermission/validate.go +++ b/pkg/registry/apis/iam/resourcepermission/validate.go @@ -24,6 +24,10 @@ func ValidateCreateAndUpdateInput(ctx context.Context, v0ResourcePerm *v0alpha1. return apierrors.NewBadRequest(fmt.Sprintf("invalid resource permission name: %s", err)) } + if grn.Name == "*" { + return apierrors.NewBadRequest(`resource name "*" is not valid: resource permissions must target a specific resource`) + } + // Validate that the group/resource/name in the name matches the spec if grn.Group != v0ResourcePerm.Spec.Resource.ApiGroup || grn.Resource != v0ResourcePerm.Spec.Resource.Resource || @@ -59,6 +63,10 @@ func ValidateDeleteInput(ctx context.Context, name string, mappers *MappersRegis return apierrors.NewBadRequest(fmt.Sprintf("invalid resource permission name: %s", err)) } + if grn.Name == "*" { + return apierrors.NewBadRequest(`resource name "*" is not valid: resource permissions must target a specific resource`) + } + // Check that the group/resource is registered and enabled if !mappers.IsEnabled(schema.GroupResource{Group: grn.Group, Resource: grn.Resource}) { return apierrors.NewBadRequest(fmt.Sprintf("unknown or disabled group/resource %s/%s", grn.Group, grn.Resource)) diff --git a/pkg/registry/apis/iam/resourcepermission/validate_test.go b/pkg/registry/apis/iam/resourcepermission/validate_test.go index dad00e66254ee..9099897b74a6b 100644 --- a/pkg/registry/apis/iam/resourcepermission/validate_test.go +++ b/pkg/registry/apis/iam/resourcepermission/validate_test.go @@ -42,6 +42,29 @@ func TestValidateOnCreate(t *testing.T) { }, want: errInvalidName, }, + { + name: "wildcard resource name - should fail", + obj: &iamv0alpha1.ResourcePermission{ + ObjectMeta: v1.ObjectMeta{ + Name: "folder.grafana.app-folders-*", + }, + Spec: iamv0alpha1.ResourcePermissionSpec{ + Resource: iamv0alpha1.ResourcePermissionspecResource{ + ApiGroup: "folder.grafana.app", + Resource: "folders", + Name: "*", + }, + Permissions: []iamv0alpha1.ResourcePermissionspecPermission{ + { + Kind: iamv0alpha1.ResourcePermissionSpecPermissionKindBasicRole, + Name: "Editor", + Verb: "edit", + }, + }, + }, + }, + want: errInvalidSpec, + }, { name: "mismatched name and spec - should fail", obj: &iamv0alpha1.ResourcePermission{ @@ -180,6 +203,11 @@ func TestValidateDeleteInput(t *testing.T) { objName: "some-invalid-name", want: errInvalidName, }, + { + name: "wildcard resource name - should fail", + objName: "folder.grafana.app-folders-*", + want: errInvalidSpec, + }, { name: "enabled group/resource (folder) - should pass", objName: "folder.grafana.app-folders-test_folder", diff --git a/pkg/services/accesscontrol/resourcepermissions/error.go b/pkg/services/accesscontrol/resourcepermissions/error.go index 258f563413d08..40054b8445448 100644 --- a/pkg/services/accesscontrol/resourcepermissions/error.go +++ b/pkg/services/accesscontrol/resourcepermissions/error.go @@ -9,6 +9,7 @@ const ( invalidAssignmentMessage = `Assignment [{{ .Public.assignment }}] is invalid for this resource type` invalidParamMessage = `Param [{{ .Public.param }}] is invalid` invalidRequestBody = `Request body is invalid: {{ .Public.reason }}` + invalidResourceIDMessage = `Resource ID [{{ .Public.resourceID }}] is not valid: wildcard "*" is not allowed` ) var ( @@ -20,6 +21,8 @@ var ( MustTemplate(invalidPermissionMessage, errutil.WithPublic(invalidPermissionMessage)) ErrInvalidAssignment = errutil.BadRequest("resourcePermissions.invalidAssignment"). MustTemplate(invalidAssignmentMessage, errutil.WithPublic(invalidAssignmentMessage)) + ErrInvalidResourceID = errutil.BadRequest("resourcePermissions.invalidResourceID"). + MustTemplate(invalidResourceIDMessage, errutil.WithPublic(invalidResourceIDMessage)) ) func ErrInvalidParamData(param string, err error) errutil.TemplateData { @@ -54,3 +57,11 @@ func ErrInvalidAssignmentData(assignment string) errutil.TemplateData { }, } } + +func ErrInvalidResourceIDData(resourceID string) errutil.TemplateData { + return errutil.TemplateData{ + Public: map[string]any{ + "resourceID": resourceID, + }, + } +} diff --git a/pkg/services/accesscontrol/resourcepermissions/service.go b/pkg/services/accesscontrol/resourcepermissions/service.go index 55cdb84d9bbc0..e70f8976b85c3 100644 --- a/pkg/services/accesscontrol/resourcepermissions/service.go +++ b/pkg/services/accesscontrol/resourcepermissions/service.go @@ -425,6 +425,10 @@ func (s *Service) validateResource(ctx context.Context, orgID int64, resourceID ctx, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.validateResource") defer span.End() + if resourceID == "*" { + return ErrInvalidResourceID.Build(ErrInvalidResourceIDData(resourceID)) + } + if s.options.ResourceValidator != nil { return s.options.ResourceValidator(ctx, orgID, resourceID) }
← Back to Alerts View on GitHub →