Authorization bypass / Access control: Denial of watch on ResourcePermissions

HIGH
grafana/grafana
Commit: dfe5e2656b21
Affected: Grafana 12.4.0 (and potentially earlier 12.4.x releases that pull this IAM authorizer logic)",
2026-04-15 00:01 UTC

Description

The commit adds a temporary authorization rule that explicitly denies watch operations on ResourcePermissions while allowing other operations. This blocks the watch exposure for ResourcePermissions, addressing an access-control weakness where watch streams could reveal permission-related data. It is a targeted code change (not a dependency bump) intended to fix an authorization bypass risk, by injecting a Deny path for the watch verb on ResourcePermissions at the API layer. The change relies on a storage-layer authorization for non-watch operations, with watch explicitly denied via a custom AuthorizerFunc.

Proof of Concept

PoC outline: Prereqs: - A Grafana 12.4.0 deployment including this commit (or a build containing the blockWatchAuthorizer) and a client with read/list rights on ResourcePermissions but not watch rights. - A Grafana API endpoint that supports Kubernetes-style watch semantics for ResourcePermissions (per the commit, the resource is ResourcePermissionInfo). 1) Before fix (for contrast): If a client can access ResourcePermissions with get/list rights but no explicit watch denial, attempting to watch ResourcePermissions would yield a stream of events. 2) After fix (with this commit): The watch operation is denied by the blockWatchAuthorizer, returning a 403 Forbidden with a reason string. Example curl (assuming Grafana API uses a watch-like endpoint on ResourcePermissions): # Acquire a token for a user with read/list on ResourcePermissions but no watch TOKEN="<token>" # Attempt to watch ResourcePermissions curl -i -N -H "Authorization: Bearer $TOKEN" \ -H "Accept: application/json" \ "https://grafana.example.com/api/registry/iam/resourcepermissions/watch" Expected outcomes: - Pre-fix (not in this build): HTTP 200 with a streaming JSON event payload (e.g., {"type":"ADDED","object":{...}}). - Post-fix (this commit): HTTP 403 Forbidden with body similar to {"error":"watch operation is disabled for ResourcePermissions"}. Code snippet (Python) to illustrate streaming vs. denial: import requests TOKEN = '<token>' headers = {'Authorization': f'Bearer {TOKEN}'} resp = requests.get('https://grafana.example.com/api/registry/iam/resourcepermissions/watch', headers=headers, stream=True) print('Status:', resp.status_code) for line in resp.iter_lines(): if line: print(line.decode()) This PoC demonstrates that attempting to watch ResourcePermissions will be denied after the fix, whereas it could have yielded a stream prior to the change.

Commit Details

Author: mohammad-hamid

Date: 2026-04-14 23:01 UTC

Message:

Use block watch authorizer (#122602) block watch authorizor

Triage Assessment

Vulnerability Type: Authorization bypass / Access control

Confidence: HIGH

Reasoning:

The commit introduces a temporary authorization rule that explicitly denies watch operations on ResourcePermissions while permitting other operations. This directly addresses an authorization/control issue by preventing potentially sensitive watch access, indicating a security fix (blocked watch exposure).

Verification Assessment

Vulnerability Type: Authorization bypass / Access control: Denial of watch on ResourcePermissions

Confidence: HIGH

Affected Versions: Grafana 12.4.0 (and potentially earlier 12.4.x releases that pull this IAM authorizer logic)",

Code Diff

diff --git a/pkg/registry/apis/iam/authorizer.go b/pkg/registry/apis/iam/authorizer.go index a0d2f65bd2bb7..9b1bb1fd0e944 100644 --- a/pkg/registry/apis/iam/authorizer.go +++ b/pkg/registry/apis/iam/authorizer.go @@ -63,11 +63,28 @@ func newIAMAuthorizer( legacyAuthorizer := gfauthorizer.NewResourceAuthorizer(legacyAccessClient) resourceAuthorizer["display"] = legacyAuthorizer + // Temporary security fix: Block Watch on ResourcePermissions until proper filtering is implemented + blockWatchAuthorizer := authorizer.AuthorizerFunc(func( + ctx context.Context, attr authorizer.Attributes, + ) (authorized authorizer.Decision, reason string, err error) { + if !attr.IsResourceRequest() { + return authorizer.DecisionNoOpinion, "", nil + } + + // Block Watch requests + if attr.GetVerb() == "watch" { + return authorizer.DecisionDeny, "watch operation is disabled for ResourcePermissions", nil + } + + // Allow all other operations (handled by storage layer authorization) + return authorizer.DecisionAllow, "", nil + }) + // Access specific resources authorizer := gfauthorizer.NewResourceAuthorizer(accessClient) resourceAuthorizer[iamv0.RoleInfo.GetName()] = roleApiInstaller.GetAuthorizer() resourceAuthorizer[iamv0.TeamLBACRuleInfo.GetName()] = teamLbacApiInstaller.GetAuthorizer() - resourceAuthorizer[iamv0.ResourcePermissionInfo.GetName()] = allowAuthorizer // Handled by the backend wrapper + resourceAuthorizer[iamv0.ResourcePermissionInfo.GetName()] = blockWatchAuthorizer // Block Watch, allow others (storage-layer handles authorization) resourceAuthorizer[iamv0.RoleBindingInfo.GetName()] = authorizer resourceAuthorizer[iamv0.ServiceAccountResourceInfo.GetName()] = newServiceAccountAuthorizer(accessClient) resourceAuthorizer[iamv0.UserResourceInfo.GetName()] = newUserAuthorizer(accessClient)
← Back to Alerts View on GitHub →