Privilege escalation / Authorization bypass
Description
The commit refactors access control to keep permissions scoped to resource types (folders vs dashboards) and updates related roles/tests. Previously, some permissions were granted with broad dashboard-wide scopes (e.g., ScopeDashboardsAll or similar), which could enable privilege escalation or unauthorized access by reading/acting on dashboards across folders. The patch replaces dashboard-wide scopes with folder-scoped values (e.g., ScopeFoldersAll or ScopeFoldersProvider UID) for relevant actions and updates annotation scope resolution to rely on folder scopes. This reduces the risk of over-broad permissions leaking across folder boundaries and fixes an authorization-scoping mismatch.
Proof of Concept
Proof-of-concept (pre-fix behavior)
Prerequisites:
- Grafana server running version <= 12.4.0 with a role that has broad dashboard scope permissions (e.g., ActionDashboardsRead with ScopeDashboardsAll).
- A dashboard located in a restricted folder (e.g., Folder-B) that the attacker should not access.
- An authenticated user bound to that broad-scope role and an API token generated for that user.
Exploit steps (pre-fix behavior):
1. Obtain a token for a user whose role includes broad dashboard access (ScopeDashboardsAll).
2. Attempt to read a restricted dashboard in Folder-B:
GET https://grafana.example.com/api/dashboards/uid/{DashboardUID}
Headers: Authorization: Bearer {Token}
3. Expectation (pre-fix): The request succeeds, returning the dashboard data from Folder-B despite folder-level restrictions, because the scope is evaluated against dashboards (not per-folder).
Expected result after the fix:
- The same request should be denied (403 Forbidden) or otherwise blocked, since permissions are now evaluated against the folder scope (ScopeFoldersAll) rather than dashboard-wide scope, preventing cross-folder privilege escalation.
Note: This PoC assumes a server with the pre-fix behavior. In a fixed version, the access should be constrained to the folder scope and prevent access to dashboards in other folders.
Commit Details
Author: Stephanie Hingtgen
Date: 2026-04-07 18:00 UTC
Message:
Dashboards: Refactor to keep perms separated (#122041)
Triage Assessment
Vulnerability Type: Privilege escalation / Authorization bypass
Confidence: HIGH
Reasoning:
The commit refactors access control scope usage to separate permissions by their resource type (folders vs dashboards) and updates related roles and tests. This changes which resources can be accessed with given actions, reducing the risk of over-broad permissions and potential privilege escalation or unauthorized access. It addresses authorization scope handling, a security relevance vulnerability in access control configurations.
Verification Assessment
Vulnerability Type: Privilege escalation / Authorization bypass
Confidence: HIGH
Affected Versions: <=12.4.0
Code Diff
diff --git a/pkg/api/accesscontrol.go b/pkg/api/accesscontrol.go
index 3d36d393824d7..ee24219a36cb0 100644
--- a/pkg/api/accesscontrol.go
+++ b/pkg/api/accesscontrol.go
@@ -6,11 +6,13 @@ import (
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards"
+ "github.com/grafana/grafana/pkg/services/dashboardsnapshots"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/libraryelements"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
+ "github.com/grafana/grafana/pkg/services/publicdashboards"
"github.com/grafana/grafana/pkg/tsdb/grafanads"
)
@@ -309,8 +311,8 @@ func (hs *HTTPServer) declareFixedRoles() error {
Description: "Create dashboards under the root folder.",
Group: "Dashboards",
Permissions: []ac.Permission{
- {Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)},
- {Action: dashboards.ActionDashboardsCreate, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)},
+ {Action: folder.ActionFoldersRead, Scope: folder.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)},
+ {Action: dashboards.ActionDashboardsCreate, Scope: folder.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)},
},
},
Grants: []string{"Editor"},
@@ -338,7 +340,7 @@ func (hs *HTTPServer) declareFixedRoles() error {
Permissions: ac.ConcatPermissions(dashboardsReaderRole.Role.Permissions, []ac.Permission{
{Action: dashboards.ActionDashboardsWrite, Scope: dashboards.ScopeDashboardsAll},
{Action: dashboards.ActionDashboardsDelete, Scope: dashboards.ScopeDashboardsAll},
- {Action: dashboards.ActionDashboardsCreate, Scope: dashboards.ScopeFoldersAll},
+ {Action: dashboards.ActionDashboardsCreate, Scope: folder.ScopeFoldersAll},
{Action: dashboards.ActionDashboardsPermissionsRead, Scope: dashboards.ScopeDashboardsAll},
{Action: dashboards.ActionDashboardsPermissionsWrite, Scope: dashboards.ScopeDashboardsAll},
}),
@@ -353,7 +355,7 @@ func (hs *HTTPServer) declareFixedRoles() error {
Description: "Create folders under root level",
Group: "Folders",
Permissions: []ac.Permission{
- {Action: dashboards.ActionFoldersCreate, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID)},
+ {Action: folder.ActionFoldersCreate, Scope: folder.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID)},
},
},
Grants: []string{"Editor"},
@@ -368,8 +370,8 @@ func (hs *HTTPServer) declareFixedRoles() error {
Description: "Read all folders and dashboards.",
Group: "Folders",
Permissions: []ac.Permission{
- {Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersAll},
- {Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeFoldersAll},
+ {Action: folder.ActionFoldersRead, Scope: folder.ScopeFoldersAll},
+ {Action: dashboards.ActionDashboardsRead, Scope: folder.ScopeFoldersAll},
},
},
Grants: []string{"Admin"},
@@ -384,7 +386,7 @@ func (hs *HTTPServer) declareFixedRoles() error {
Group: "Folders",
Hidden: true,
Permissions: []ac.Permission{
- {Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)},
+ {Action: folder.ActionFoldersRead, Scope: folder.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)},
},
},
Grants: []string{string(org.RoleViewer)},
@@ -399,14 +401,14 @@ func (hs *HTTPServer) declareFixedRoles() error {
Permissions: ac.ConcatPermissions(
foldersReaderRole.Role.Permissions,
[]ac.Permission{
- {Action: dashboards.ActionFoldersCreate, Scope: dashboards.ScopeFoldersAll},
- {Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersAll},
- {Action: dashboards.ActionFoldersDelete, Scope: dashboards.ScopeFoldersAll},
- {Action: dashboards.ActionDashboardsWrite, Scope: dashboards.ScopeFoldersAll},
- {Action: dashboards.ActionDashboardsDelete, Scope: dashboards.ScopeFoldersAll},
- {Action: dashboards.ActionDashboardsCreate, Scope: dashboards.ScopeFoldersAll},
- {Action: dashboards.ActionDashboardsPermissionsRead, Scope: dashboards.ScopeFoldersAll},
- {Action: dashboards.ActionDashboardsPermissionsWrite, Scope: dashboards.ScopeFoldersAll},
+ {Action: folder.ActionFoldersCreate, Scope: folder.ScopeFoldersAll},
+ {Action: folder.ActionFoldersWrite, Scope: folder.ScopeFoldersAll},
+ {Action: folder.ActionFoldersDelete, Scope: folder.ScopeFoldersAll},
+ {Action: dashboards.ActionDashboardsWrite, Scope: folder.ScopeFoldersAll},
+ {Action: dashboards.ActionDashboardsDelete, Scope: folder.ScopeFoldersAll},
+ {Action: dashboards.ActionDashboardsCreate, Scope: folder.ScopeFoldersAll},
+ {Action: dashboards.ActionDashboardsPermissionsRead, Scope: folder.ScopeFoldersAll},
+ {Action: dashboards.ActionDashboardsPermissionsWrite, Scope: folder.ScopeFoldersAll},
}),
},
Grants: []string{"Admin"},
@@ -419,8 +421,8 @@ func (hs *HTTPServer) declareFixedRoles() error {
Description: "Create library panel under the root folder.",
Group: "Library panels",
Permissions: []ac.Permission{
- {Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)},
- {Action: libraryelements.ActionLibraryPanelsCreate, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)},
+ {Action: folder.ActionFoldersRead, Scope: folder.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)},
+ {Action: libraryelements.ActionLibraryPanelsCreate, Scope: folder.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)},
},
},
Grants: []string{"Editor"},
@@ -433,7 +435,7 @@ func (hs *HTTPServer) declareFixedRoles() error {
Description: "Read all library panels.",
Group: "Library panels",
Permissions: []ac.Permission{
- {Action: libraryelements.ActionLibraryPanelsRead, Scope: dashboards.ScopeFoldersAll},
+ {Action: libraryelements.ActionLibraryPanelsRead, Scope: folder.ScopeFoldersAll},
},
},
Grants: []string{"Admin"},
@@ -446,7 +448,7 @@ func (hs *HTTPServer) declareFixedRoles() error {
Description: "Read all library panels under the root folder.",
Group: "Library panels",
Permissions: []ac.Permission{
- {Action: libraryelements.ActionLibraryPanelsRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)},
+ {Action: libraryelements.ActionLibraryPanelsRead, Scope: folder.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)},
},
},
Grants: []string{"Viewer"},
@@ -459,9 +461,9 @@ func (hs *HTTPServer) declareFixedRoles() error {
Group: "Library panels",
Description: "Create, read, write or delete all library panels and their permissions.",
Permissions: ac.ConcatPermissions(libraryPanelsReaderRole.Role.Permissions, []ac.Permission{
- {Action: libraryelements.ActionLibraryPanelsWrite, Scope: dashboards.ScopeFoldersAll},
- {Action: libraryelements.ActionLibraryPanelsDelete, Scope: dashboards.ScopeFoldersAll},
- {Action: libraryelements.ActionLibraryPanelsCreate, Scope: dashboards.ScopeFoldersAll},
+ {Action: libraryelements.ActionLibraryPanelsWrite, Scope: folder.ScopeFoldersAll},
+ {Action: libraryelements.ActionLibraryPanelsDelete, Scope: folder.ScopeFoldersAll},
+ {Action: libraryelements.ActionLibraryPanelsCreate, Scope: folder.ScopeFoldersAll},
}),
},
Grants: []string{"Admin"},
@@ -474,9 +476,9 @@ func (hs *HTTPServer) declareFixedRoles() error {
Group: "Library panels",
Description: "Create, read, write or delete all library panels and their permissions under the root folder.",
Permissions: ac.ConcatPermissions(libraryPanelsGeneralReaderRole.Role.Permissions, []ac.Permission{
- {Action: libraryelements.ActionLibraryPanelsWrite, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)},
- {Action: libraryelements.ActionLibraryPanelsDelete, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)},
- {Action: libraryelements.ActionLibraryPanelsCreate, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)},
+ {Action: libraryelements.ActionLibraryPanelsWrite, Scope: folder.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)},
+ {Action: libraryelements.ActionLibraryPanelsDelete, Scope: folder.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)},
+ {Action: libraryelements.ActionLibraryPanelsCreate, Scope: folder.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)},
}),
},
Grants: []string{"Editor"},
@@ -489,7 +491,7 @@ func (hs *HTTPServer) declareFixedRoles() error {
Description: "Create, write or disable a public dashboard.",
Group: "Dashboards",
Permissions: []ac.Permission{
- {Action: dashboards.ActionDashboardsPublicWrite, Scope: dashboards.ScopeDashboardsAll},
+ {Action: publicdashboards.ActionDashboardsPublicWrite, Scope: dashboards.ScopeDashboardsAll},
},
},
Grants: []string{"Admin"},
@@ -528,7 +530,7 @@ func (hs *HTTPServer) declareFixedRoles() error {
Description: "Create snapshots",
Group: "Snapshots",
Permissions: []ac.Permission{
- {Action: dashboards.ActionSnapshotsCreate},
+ {Action: dashboardsnapshots.ActionSnapshotsCreate},
},
},
Grants: []string{string(org.RoleEditor)},
@@ -541,7 +543,7 @@ func (hs *HTTPServer) declareFixedRoles() error {
Description: "Delete snapshots",
Group: "Snapshots",
Permissions: []ac.Permission{
- {Action: dashboards.ActionSnapshotsDelete},
+ {Action: dashboardsnapshots.ActionSnapshotsDelete},
},
},
Grants: []string{string(org.RoleEditor)},
@@ -554,7 +556,7 @@ func (hs *HTTPServer) declareFixedRoles() error {
Description: "Read snapshots",
Group: "Snapshots",
Permissions: []ac.Permission{
- {Action: dashboards.ActionSnapshotsRead},
+ {Action: dashboardsnapshots.ActionSnapshotsRead},
},
},
Grants: []string{string(org.RoleViewer)},
@@ -568,7 +570,7 @@ func (hs *HTTPServer) declareFixedRoles() error {
Group: "Annotations",
Permissions: []ac.Permission{
{Action: ac.ActionAnnotationsRead, Scope: ac.ScopeAnnotationsTypeOrganization},
- {Action: ac.ActionAnnotationsRead, Scope: dashboards.ScopeFoldersAll},
+ {Action: ac.ActionAnnotationsRead, Scope: folder.ScopeFoldersAll},
},
},
Grants: []string{string(org.RoleAdmin)},
@@ -582,11 +584,11 @@ func (hs *HTTPServer) declareFixedRoles() error {
Group: "Annotations",
Permissions: []ac.Permission{
{Action: ac.ActionAnnotationsCreate, Scope: ac.ScopeAnnotationsTypeOrganization},
- {Action: ac.ActionAnnotationsCreate, Scope: dashboards.ScopeFoldersAll},
+ {Action: ac.ActionAnnotationsCreate, Scope: folder.ScopeFoldersAll},
{Action: ac.ActionAnnotationsDelete, Scope: ac.ScopeAnnotationsTypeOrganization},
- {Action: ac.ActionAnnotationsDelete, Scope: dashboards.ScopeFoldersAll},
+ {Action: ac.ActionAnnotationsDelete, Scope: folder.ScopeFoldersAll},
{Action: ac.ActionAnnotationsWrite, Scope: ac.ScopeAnnotationsTypeOrganization},
- {Action: ac.ActionAnnotationsWrite, Scope: dashboards.ScopeFoldersAll},
+ {Action: ac.ActionAnnotationsWrite, Scope: folder.ScopeFoldersAll},
},
},
Grants: []string{string(org.RoleAdmin)},
diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go
index 25e83854d7b16..37ede180a1a9e 100644
--- a/pkg/api/annotations.go
+++ b/pkg/api/annotations.go
@@ -585,14 +585,14 @@ func AnnotationTypeScopeResolver(annotationsRepo annotations.Repository, feature
scopes := []string{dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dashboard.UID)}
// Append dashboard parent scopes if dashboard is in a folder or the general scope if dashboard is not in a folder
if dashboard.FolderUID != "" {
- scopes = append(scopes, dashboards.ScopeFoldersProvider.GetResourceScopeUID(dashboard.FolderUID))
- inheritedScopes, err := dashboards.GetInheritedScopes(ctx, orgID, dashboard.FolderUID, folderSvc)
+ scopes = append(scopes, folder.ScopeFoldersProvider.GetResourceScopeUID(dashboard.FolderUID))
+ inheritedScopes, err := folder.GetInheritedScopes(ctx, orgID, dashboard.FolderUID, folderSvc)
if err != nil {
return nil, err
}
scopes = append(scopes, inheritedScopes...)
} else {
- scopes = append(scopes, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID))
+ scopes = append(scopes, folder.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID))
}
return scopes, nil
})
diff --git a/pkg/api/annotations_test.go b/pkg/api/annotations_test.go
index 81f323ca3a10d..e14018d6ccd70 100644
--- a/pkg/api/annotations_test.go
+++ b/pkg/api/annotations_test.go
@@ -77,7 +77,7 @@ func TestAPI_Annotations(t *testing.T) {
path: "/api/annotations/2",
method: http.MethodGet,
expectedCode: http.StatusOK,
- permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(folderUID)}},
+ permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsRead, Scope: folder.ScopeFoldersProvider.GetResourceScopeUID(folderUID)}},
},
{
desc: "should not be able to fetch dashboard annotation by id with the old dashboard scope",
@@ -119,7 +119,7 @@ func TestAPI_Annotations(t *testing.T) {
path: "/api/annotations/2",
method: http.MethodPut,
expectedCode: http.StatusOK,
- permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsWrite, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(folderUID)}},
+ permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsWrite, Scope: folder.ScopeFoldersProvider.GetResourceScopeUID(folderUID)}},
},
{
desc: "should not be able to update dashboard annotation with the old dashboard scope",
@@ -161,7 +161,7 @@ func TestAPI_Annotations(t *testing.T) {
path: "/api/annotations/2",
method: http.MethodPatch,
expectedCode: http.StatusOK,
- permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsWrite, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(folderUID)}},
+ permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotations
... [truncated]