Authorization bypass / Access control: Denial of watch on ResourcePermissions
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)