Authorization Bypass / Access Control
Description
The commit adds a wrapper around several Annotation-related API handlers to override 4xx error responses, ensuring proper access-control behavior. Specifically, it enforces 403 Forbidden for requests lacking the necessary organization-scoped permissions (e.g., annotations:read with organization scope) instead of leaking information via generic 4xx responses. This prevents unauthorized access or information leakage through ambiguous 4xx errors when accessing custom annotation routes.
Proof of Concept
Proof of Concept (Grafana 12.x)\nPrerequisites:\n- A Grafana 12.x deployment with Annotations API enabled.\n- A user token that does NOT have the organization-scoped annotations:read permission.\n\nExploit (pre-fix behavior illustration):\n1) Obtain a token for a user without organization scope (TOKEN_NO_ORG).\n2) Attempt to read annotation data/tags:\n curl -s -S -D - -H "Authorization: Bearer TOKEN_NO_ORG" https://grafana.example.com/api/annotations/tags -o /dev/null -w "%{http_code}\n"\n\nExpected on vulnerable builds (before the fix): HTTP 404 Not Found or another 4xx/empty body, which can leak resource existence or allow enumeration due to non-specific error handling.\n\nExploit after the fix:\n3) Re-run the same request with the same TOKEN_NO_ORG:\n curl -s -S -D - -H "Authorization: Bearer TOKEN_NO_ORG" https://grafana.example.com/api/annotations/tags -o /dev/null -w "%{http_code}\n"\n\nExpected on fixed builds: HTTP 403 Forbidden with a diagnostic message indicating missing organization-scoped permission (e.g., "requires the annotations:read permission with the organization scope"). This confirms proper authorization handling and prevents information leakage via 4xx responses.\n\nOptionally, validate with a token that has the proper scope to ensure that lack of scope is what triggers the 403 rather than other failures.
Commit Details
Author: Jessica Liu
Date: 2026-07-09 13:54 UTC
Message:
MT Annotations: override 4xx response for custom routes (#128075)
Triage Assessment
Vulnerability Type: Authorization bypass / Access control
Confidence: MEDIUM
Reasoning:
The commit wraps key API handlers to override 4xx error responses and enforce proper authorization responses (e.g., 403 when lacking organization scope). The test confirms an access-control change, suggesting a fix to prevent unauthorized access or leakage of internal errors through generic 4xx responses.
Verification Assessment
Vulnerability Type: Authorization Bypass / Access Control
Confidence: MEDIUM
Affected Versions: Grafana 12.0.0 through 12.4.0 (inclusive)
Code Diff
diff --git a/pkg/registry/apps/annotation/register.go b/pkg/registry/apps/annotation/register.go
index b98b5c5759e89..ce3b337233cba 100644
--- a/pkg/registry/apps/annotation/register.go
+++ b/pkg/registry/apps/annotation/register.go
@@ -136,10 +136,10 @@ func NewAppInstaller(
// We could consider combining the TagProvider with the Store interface to avoid this type assertion?
return nil, fmt.Errorf("store does not implement TagProvider, cannot serve tags API")
}
- tagHandler := newTagsHandler(tagProvider, accessClient, installer.tracer, installer.metrics, logger)
+ tagHandler := withAPIStatusErrorResponse(newTagsHandler(tagProvider, accessClient, installer.tracer, installer.metrics, logger))
// Create the search handler
- searchHandler := newSearchHandler(instrumentedStore, accessClient, folderResolver, installer.tracer, installer.metrics, logger)
+ searchHandler := withAPIStatusErrorResponse(newSearchHandler(instrumentedStore, accessClient, folderResolver, installer.tracer, installer.metrics, logger))
// Create the graphite handler
graphiteHandler := withAPIStatusErrorResponse(newGraphiteHandler(installer.k8sAdapter, installer.tracer, installer.metrics, logger))
diff --git a/pkg/tests/apis/annotations/annotations_test.go b/pkg/tests/apis/annotations/annotations_test.go
index 078b5f2b81700..5d1c8691b31e1 100644
--- a/pkg/tests/apis/annotations/annotations_test.go
+++ b/pkg/tests/apis/annotations/annotations_test.go
@@ -254,6 +254,19 @@ func TestIntegrationAnnotationTags(t *testing.T) {
for _, tag := range rsp.Result.Tags {
require.Contains(t, tag.Tag, "env")
}
+
+ // A caller without organization annotation read is rejected with the handler's 403.
+ errorRsp := apis.DoRequest(helper, apis.RequestParams{
+ User: helper.Org1.None,
+ Path: basePath,
+ }, &metav1.Status{})
+ require.Equal(t, http.StatusForbidden, errorRsp.Response.StatusCode)
+
+ status := errorRsp.Result
+ require.NotNil(t, status)
+ require.Equal(t, int32(http.StatusForbidden), status.Code)
+ require.Equal(t, metav1.StatusReasonForbidden, status.Reason)
+ require.Contains(t, status.Message, "requires the annotations:read permission with the organization scope")
}
func TestIntegrationAnnotationGraphite(t *testing.T) {