Privilege Escalation via RoleBinding subject kind

MEDIUM
grafana/grafana
Commit: 4f3fec159530
Affected: <=12.4.0 (IAM v0alpha1 role binding subject kinds)
2026-04-03 21:49 UTC

Description

The commit removes the BasicRole subject kind from RoleBinding subject kinds across API schemas and generated code. Previously, BasicRole could be used as a valid subject in role bindings, and the translator mapped BasicRole to an internal assignee/role construct. This created a potential privilege-escalation path where an attacker with permission to create or mutate RoleBindings could grant a Role to a BasicRole subject, effectively elevating permissions or bypassing intended access controls. The change hardens the IAM model by allowing only User, ServiceAccount, and Team as valid subjects and by removing the BasicRole path from the code and tests.

Proof of Concept

Proof-of-concept exploit (pre-fix behavior): 1) Preconditions: Grafana instance with IAM enabled (Grafana 12.4.0 or earlier), and an attacker with permission to mutate role bindings in a namespace. 2) Attacker crafts a mutation to create a RoleBinding with SubjectKind: BasicRole, SubjectName: basic_viewer, RoleKind: Role, RoleName: foo_bar. This would be accepted by the API prior to the fix (the code contains a path for BasicRole and maps it to an internal assignee/role tuple). 3) After the mutation, query the authorization tuples to observe the mapping, e.g. read with relation: Assignee and object: role:foo_bar. The response would include a tuple whose User is: role:basic_viewer#assignee, demonstrating that the BasicRole subject has been granted access rights via the RoleBinding. 4) Result: The attacker gains or augments permissions through the bound Role (depending on Role permissions), constituting privilege escalation. Mitigation after the fix: BasicRole is no longer accepted as a valid subject kind; attempts to create RoleBindings with BasicRole would be rejected, preventing this escalation path. Note: This PoC reflects pre-fix behavior observed in the repository diffs (tests and translator mappings show BasicRole being treated as a valid subject). Do not run on production systems without explicit authorization.

Commit Details

Author: Gabriel MABILLE

Date: 2026-03-16 16:10 UTC

Message:

IAM: remove BasicRole from RoleBinding subject kinds (#120134) * IAM: remove BasicRole from RoleBinding subject kinds Made-with: Cursor * Lint

Triage Assessment

Vulnerability Type: Privilege Escalation

Confidence: MEDIUM

Reasoning:

The commit removes the BasicRole subject kind from RoleBinding subject kinds across API schemas and generated code. This narrows allowed subject types (User, ServiceAccount, Team) and eliminates a potentially privileged/undocumented role path that could be misused to bypass access controls or assign permissions via a BasicRole. It is a security-conscious restriction rather than a feature or purely cosmetic change.

Verification Assessment

Vulnerability Type: Privilege Escalation via RoleBinding subject kind

Confidence: MEDIUM

Affected Versions: <=12.4.0 (IAM v0alpha1 role binding subject kinds)

Code Diff

diff --git a/apps/iam/kinds/v0alpha1/rolebindingspec.cue b/apps/iam/kinds/v0alpha1/rolebindingspec.cue index 4f79094adde5d..bd28ec3c8de28 100644 --- a/apps/iam/kinds/v0alpha1/rolebindingspec.cue +++ b/apps/iam/kinds/v0alpha1/rolebindingspec.cue @@ -3,7 +3,7 @@ package v0alpha1 RoleBindingSpec: { #Subject: { // kind of the identity getting the permission - kind: "User" | "ServiceAccount" | "Team" | "BasicRole" + kind: "User" | "ServiceAccount" | "Team" // uid of the identity name: string } @@ -21,7 +21,7 @@ RoleBindingSpec: { GlobalRoleBindingSpec: { #Subject: { // kind of the identity getting the permission - kind: "User" | "ServiceAccount" | "Team" | "BasicRole" + kind: "User" | "ServiceAccount" | "Team" // uid of the identity name: string } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/globalrolebinding_spec_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/globalrolebinding_spec_gen.go index 61b5f620b9ff3..0bbe33457bda0 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/globalrolebinding_spec_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/globalrolebinding_spec_gen.go @@ -66,7 +66,6 @@ const ( GlobalRoleBindingSpecSubjectKindUser GlobalRoleBindingSpecSubjectKind = "User" GlobalRoleBindingSpecSubjectKindServiceAccount GlobalRoleBindingSpecSubjectKind = "ServiceAccount" GlobalRoleBindingSpecSubjectKindTeam GlobalRoleBindingSpecSubjectKind = "Team" - GlobalRoleBindingSpecSubjectKindBasicRole GlobalRoleBindingSpecSubjectKind = "BasicRole" ) // OpenAPIModelName returns the OpenAPI model name for GlobalRoleBindingSpecSubjectKind. diff --git a/apps/iam/pkg/apis/iam/v0alpha1/rolebinding_spec_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/rolebinding_spec_gen.go index 4f94c299e9e67..89de8d5c84653 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/rolebinding_spec_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/rolebinding_spec_gen.go @@ -64,7 +64,6 @@ const ( RoleBindingSpecSubjectKindUser RoleBindingSpecSubjectKind = "User" RoleBindingSpecSubjectKindServiceAccount RoleBindingSpecSubjectKind = "ServiceAccount" RoleBindingSpecSubjectKindTeam RoleBindingSpecSubjectKind = "Team" - RoleBindingSpecSubjectKindBasicRole RoleBindingSpecSubjectKind = "BasicRole" ) // OpenAPIModelName returns the OpenAPI model name for RoleBindingSpecSubjectKind. diff --git a/pkg/services/authz/proto/v1/extention.proto b/pkg/services/authz/proto/v1/extention.proto index 0d26d1549d1a1..4fe0484ff388c 100644 --- a/pkg/services/authz/proto/v1/extention.proto +++ b/pkg/services/authz/proto/v1/extention.proto @@ -92,7 +92,7 @@ message DeleteUserOrgRoleOperation { } message CreateRoleBindingOperation { - // kind of the identity getting the permission (User/Team/ServiceAccount/BasicRole) + // kind of the identity getting the permission (User/Team/ServiceAccount) string subject_kind = 1; // uid of the identity string subject_name = 2; @@ -103,7 +103,7 @@ message CreateRoleBindingOperation { } message DeleteRoleBindingOperation { - // kind of the identity getting the permission (User/Team/ServiceAccount/BasicRole) + // kind of the identity getting the permission (User/Team/ServiceAccount) string subject_kind = 1; // uid of the identity string subject_name = 2; diff --git a/pkg/services/authz/zanzana/server/reconciler/translators_test.go b/pkg/services/authz/zanzana/server/reconciler/translators_test.go index d302ee04901cf..417023cefa4a1 100644 --- a/pkg/services/authz/zanzana/server/reconciler/translators_test.go +++ b/pkg/services/authz/zanzana/server/reconciler/translators_test.go @@ -177,12 +177,6 @@ func TestTranslateGlobalRoleBindingToTuples(t *testing.T) { subjectName: "team1", expectedUser: "team:team1#member", }, - { - name: "basic role subject", - subjectKind: iamv0.GlobalRoleBindingSpecSubjectKindBasicRole, - subjectName: "basic_viewer", - expectedUser: "role:basic_viewer#assignee", - }, } for _, tt := range tests { @@ -236,12 +230,6 @@ func TestTranslateRoleBindingToTuples(t *testing.T) { subjectName: "team1", expectedUser: "team:team1#member", }, - { - name: "basic role subject", - subjectKind: iamv0.RoleBindingSpecSubjectKindBasicRole, - subjectName: "basic_viewer", - expectedUser: "role:basic_viewer#assignee", - }, } for _, tt := range tests { @@ -766,7 +754,6 @@ func TestTranslatedTuplesAreSchemaValid(t *testing.T) { {iamv0.RoleBindingSpecSubjectKindUser, "uid1"}, {iamv0.RoleBindingSpecSubjectKindServiceAccount, "sa1"}, {iamv0.RoleBindingSpecSubjectKindTeam, "team1"}, - {iamv0.RoleBindingSpecSubjectKindBasicRole, "basic_viewer"}, } for _, s := range subjects { @@ -820,7 +807,6 @@ func TestTranslatedTuplesAreSchemaValid(t *testing.T) { {iamv0.GlobalRoleBindingSpecSubjectKindUser, "uid1"}, {iamv0.GlobalRoleBindingSpecSubjectKindServiceAccount, "sa1"}, {iamv0.GlobalRoleBindingSpecSubjectKindTeam, "team1"}, - {iamv0.GlobalRoleBindingSpecSubjectKindBasicRole, "basic_viewer"}, } for _, s := range subjects { diff --git a/pkg/services/authz/zanzana/server/server_mutate_rolebindings_test.go b/pkg/services/authz/zanzana/server/server_mutate_rolebindings_test.go index 810298009d576..bd773d5d93670 100644 --- a/pkg/services/authz/zanzana/server/server_mutate_rolebindings_test.go +++ b/pkg/services/authz/zanzana/server/server_mutate_rolebindings_test.go @@ -77,34 +77,4 @@ func TestIntegrationServerMutateRoleBindings(t *testing.T) { require.NoError(t, err) require.Len(t, res.Tuples, 0) }) - - t.Run("should assign role to basic role", func(t *testing.T) { - _, err := srv.Mutate(newContextWithZanzanaUpdatePermission(), &v1.MutateRequest{ - Namespace: "default", - Operations: []*v1.MutateOperation{ - { - Operation: &v1.MutateOperation_CreateRoleBinding{ - CreateRoleBinding: &v1.CreateRoleBindingOperation{ - SubjectKind: "BasicRole", - SubjectName: "basic_viewer", - RoleKind: "Role", - RoleName: "foo_bar", - }, - }, - }, - }, - }) - require.NoError(t, err) - - res, err := srv.Read(newContextWithNamespace(), &v1.ReadRequest{ - Namespace: "default", - TupleKey: &v1.ReadRequestTupleKey{ - Relation: common.RelationAssignee, - Object: "role:foo_bar", - }, - }) - require.NoError(t, err) - require.Len(t, res.Tuples, 1) - require.Equal(t, "role:basic_viewer#assignee", res.Tuples[0].Key.User) - }) } diff --git a/pkg/services/authz/zanzana/tuple_helpers.go b/pkg/services/authz/zanzana/tuple_helpers.go index bce29417a7cb8..b5171a80acda7 100644 --- a/pkg/services/authz/zanzana/tuple_helpers.go +++ b/pkg/services/authz/zanzana/tuple_helpers.go @@ -147,9 +147,6 @@ func GetRoleBindingTuple(subjectKind string, subjectName string, roleName string subjectRelation = RelationTeamMember case string(iamv0.RoleBindingSpecSubjectKindServiceAccount): zanzanaType = TypeServiceAccount - case string(iamv0.RoleBindingSpecSubjectKindBasicRole): - zanzanaType = TypeRole - subjectRelation = RelationAssignee default: return nil, fmt.Errorf("invalid subject kind: %s", subjectKind) }
← Back to Alerts View on GitHub →