Information Disclosure
Description
The commit adds logic to strip folder-related annotations from V2 resource exports when sharing externally, mitigating an information disclosure risk where internal folder metadata (e.g., AnnoKeyFolder, AnnoKeyFolderTitle, AnnoKeyFolderUrl, and related permission annotations) could be leaked to external consumers. Prior to this change, V2 resource exports could include folder metadata in metadata.annotations, potentially revealing folder structure and access controls. The fix ensures such folder annotations are removed from exported dashboards/resources, thereby reducing leakage of internal folder metadata.
Proof of Concept
Pre-fix (vulnerable behavior): The V2 resource export payload could contain folder-related annotations. Example export snippet that could be exposed to an external consumer:
{
"metadata": {
"annotations": {
"AnnoKeyFolder": "folder-uid",
"AnnoKeyFolderTitle": "My Folder",
"AnnoKeyFolderUrl": "/dashboards/f/my-folder",
"AnnoKeyGrantPermissions": "default"
}
},
"spec": { /* dashboard/resource spec */ }
}
Attack vector: An external recipient with access to the shared/exported resource could map dashboards to internal folders and infer folder structure and permissions based on these annotation values.
Reproduction steps (conceptual):
1) Create or obtain a dashboard/resource exported in V2Resource format while sharing externally.
2) Inspect the exported JSON for metadata.annotations keys AnnoKeyFolder, AnnoKeyFolderTitle, AnnoKeyFolderUrl (and related keys).
3) Observe folder metadata leakage.
Post-fix behavior (as implemented in the commit): The export path strips folder annotations from metadata.annotations (and may drop related keys when sharing externally), so the leaked folder metadata will not be present in the exported payload.
Commit Details
Author: Haris Rozajac
Date: 2026-06-24 18:48 UTC
Message:
Dashboard Export: strip folder annotations from v2 resource export (#127108)
strip folder annotations from v2 resource export
Triage Assessment
Vulnerability Type: Information Disclosure
Confidence: HIGH
Reasoning:
The change strips folder-related annotations (e.g., AnnoKeyFolder, AnnoKeyFolderTitle, AnnoKeyFolderUrl) from V2 resource exports, particularly when sharing externally. This prevents leaking internal folder metadata through exported resources, addressing potential information disclosure and unintended exposure of structure/permissions.
Verification Assessment
Vulnerability Type: Information Disclosure
Confidence: HIGH
Affected Versions: Grafana 12.4.0 and earlier (V2 resource export path)
Code Diff
diff --git a/public/app/features/dashboard-scene/sharing/ShareExportTab.test.tsx b/public/app/features/dashboard-scene/sharing/ShareExportTab.test.tsx
index 4c959116d19b8..380cd077b9750 100644
--- a/public/app/features/dashboard-scene/sharing/ShareExportTab.test.tsx
+++ b/public/app/features/dashboard-scene/sharing/ShareExportTab.test.tsx
@@ -5,6 +5,12 @@ import {
defaultQueryGroupKind,
defaultVizConfigSpec,
} from '@grafana/schema/apis/dashboard.grafana.app/v2';
+import {
+ AnnoKeyFolder,
+ AnnoKeyFolderTitle,
+ AnnoKeyFolderUrl,
+ AnnoKeyGrantPermissions,
+} from 'app/features/apiserver/types';
import * as dashboardApiModule from 'app/features/dashboard/api/dashboard_api';
import { ExportFormat, type DashboardWithAccessInfo } from 'app/features/dashboard/api/types';
import { type DashboardDataDTO } from 'app/types/dashboard';
@@ -210,6 +216,58 @@ describe('ShareExportTab', () => {
}
});
+ it('should strip folder annotations when sharing externally is off', async () => {
+ mockGetDashboardAPI(mockV1Spec, {
+ ...mockV2ResourceResponse,
+ metadata: {
+ ...mockV2ResourceResponse.metadata,
+ annotations: {
+ [AnnoKeyFolder]: 'folder-uid',
+ [AnnoKeyFolderTitle]: 'My Folder',
+ [AnnoKeyFolderUrl]: '/dashboards/f/my-folder',
+ [AnnoKeyGrantPermissions]: 'default',
+ },
+ },
+ });
+
+ const tab = buildV2DashboardScenario();
+ tab.setState({ exportFormat: ExportFormat.V2Resource, isSharingExternally: false });
+
+ const result = await tab.getExportableDashboardJson();
+
+ expect('metadata' in result.json && result.json.metadata?.annotations).toEqual({
+ [AnnoKeyGrantPermissions]: 'default',
+ });
+ });
+
+ it('should strip folder annotations when sharing externally is on', async () => {
+ mockGetDashboardAPI(mockV1Spec, {
+ ...mockV2ResourceResponse,
+ metadata: {
+ ...mockV2ResourceResponse.metadata,
+ annotations: {
+ [AnnoKeyFolder]: 'folder-uid',
+ [AnnoKeyFolderTitle]: 'My Folder',
+ [AnnoKeyFolderUrl]: '/dashboards/f/my-folder',
+ [AnnoKeyGrantPermissions]: 'default',
+ },
+ },
+ });
+
+ const tab = buildV2DashboardScenario();
+ tab.setState({ exportFormat: ExportFormat.V2Resource, isSharingExternally: true });
+
+ const result = await tab.getExportableDashboardJson();
+
+ if ('metadata' in result.json) {
+ const annotations = result.json.metadata?.annotations ?? {};
+ expect(annotations[AnnoKeyFolder]).toBeUndefined();
+ expect(annotations[AnnoKeyFolderTitle]).toBeUndefined();
+ expect(annotations[AnnoKeyFolderUrl]).toBeUndefined();
+ expect(annotations[AnnoKeyGrantPermissions]).toBeUndefined();
+ }
+ });
+
it('should fetch V2 resource for V1 dashboard (server converts)', async () => {
const tab = buildV1DashboardScenario();
tab.setState({ exportFormat: ExportFormat.V2Resource });
diff --git a/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx b/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx
index e9b632045531e..7c13eb1fa943d 100644
--- a/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx
+++ b/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx
@@ -10,7 +10,7 @@ import { type SceneComponentProps, SceneObjectBase } from '@grafana/scenes';
import { type Dashboard } from '@grafana/schema';
import { type Spec as DashboardV2Spec } from '@grafana/schema/apis/dashboard.grafana.app/v2';
import { Button, ClipboardButton, CodeEditor, Modal } from '@grafana/ui';
-import { type ObjectMeta } from 'app/features/apiserver/types';
+import { AnnoKeyFolder, AnnoKeyFolderTitle, AnnoKeyFolderUrl, type ObjectMeta } from 'app/features/apiserver/types';
import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api';
import { ExportFormat } from 'app/features/dashboard/api/types';
import { isDashboardV2Spec } from 'app/features/dashboard/api/utils';
@@ -260,11 +260,23 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme
};
}
+const FOLDER_EXPORT_ANNOTATIONS = [AnnoKeyFolder, AnnoKeyFolderTitle, AnnoKeyFolderUrl] as const;
+
+function stripFolderAnnotations(annotations: Record<string, string> | undefined) {
+ if (!annotations) {
+ return;
+ }
+ for (const key of FOLDER_EXPORT_ANNOTATIONS) {
+ delete annotations[key];
+ }
+}
+
function stripMetadataForExport(metadata: ObjectMeta, isSharingExternally: boolean): Partial<ObjectMeta> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result: Record<string, any> = cloneDeep(metadata);
delete result['managedFields'];
+ stripFolderAnnotations(result['annotations']);
if (isSharingExternally) {
delete result['uid'];