Information Disclosure
Description
This commit fixes an information disclosure risk by removing internal identifiers (dashboardId and datasourceId) from analytics payloads and related types. Prior to this change, analytics events could expose internal dashboard and datasource IDs to the analytics collector or logs, enabling potential mapping or enumeration of internal resources. The patch updates the analytics emission logic to drop the internal IDs, removes deprecated/internal-id fields from dashboards and related serialization, and cleans up types to ensure internal IDs are no longer exposed. While this is primarily a data-minimization/security hardening measure, it mitigates a concrete information-disclosure surface in analytics telemetry.
Proof of Concept
PoC steps (before fix):
1) Grafana 12.4.0 configures analytics/telemetry to send events to an external collector. A dashboard view triggers an analytics payload that includes internal IDs, e.g. {
"event": "DashboardViewed",
"dashboardName": "Finance Q1",
"dashboardId": 12345,
"dashboardUid": "uid- Finance1",
"datasourceId": 67890,
"datasourceUid": "ds-6789"
}
2) An attacker who can access telemetry storage or logs (due to misconfiguration or insufficient access controls) reads the payload and learns internal IDs (dashboardId, datasourceId).
3) The attacker uses those IDs to map to internal dashboards/datasources or to craft targeted requests if internal APIs accept/accept those IDs, enabling information disclosure or reconnaissance.
4) After the fix, the payload omits dashboardId and datasourceId, so internal IDs are not exposed in analytics data.
Mitigation verification (post-fix): Ensure analytics payload logs contain no keys named dashboardId or datasourceId and that any rendering logic or payload builders no longer reference internal IDs. Attempting to trigger events should show only public identifiers (e.g., dashboardUid) in the payload.
Commit Details
Author: Michael Mandrus
Date: 2026-04-04 02:07 UTC
Message:
Analytics: Remove dashboard and data source ID (#121326)
* remove dashboard id
* remove data source id
Triage Assessment
Vulnerability Type: Information Disclosure
Confidence: MEDIUM
Reasoning:
The commit removes internal dashboard and data source IDs from analytics payloads and related types. By not exposing internal IDs, it reduces potential information disclosure or data leakage of internal identifiers in analytics events, which can be leveraged in targeted attacks. The change is a security-oriented data Minimization/obfuscation rather than a feature change.
Verification Assessment
Vulnerability Type: Information Disclosure
Confidence: MEDIUM
Affected Versions: <= 12.4.0 (prior to this commit); versions that emitted internal IDs in analytics payloads
Code Diff
diff --git a/eslint-suppressions.json b/eslint-suppressions.json
index 4660c0acc644d..392be72a98040 100644
--- a/eslint-suppressions.json
+++ b/eslint-suppressions.json
@@ -2114,7 +2114,7 @@
"count": 1
},
"@typescript-eslint/no-explicit-any": {
- "count": 24
+ "count": 23
}
},
"public/app/features/dashboard/state/PanelModel.test.ts": {
diff --git a/packages/grafana-runtime/src/analytics/types.ts b/packages/grafana-runtime/src/analytics/types.ts
index 8226a210e33ad..23a5e55fbd40b 100644
--- a/packages/grafana-runtime/src/analytics/types.ts
+++ b/packages/grafana-runtime/src/analytics/types.ts
@@ -10,7 +10,6 @@ import { type EchoEvent, type EchoEventType } from '../services/EchoSrv';
*/
export interface DashboardInfo {
/** @deprecated -- use UID not internal ID */
- dashboardId: number;
dashboardUid: string;
dashboardName: string;
folderName?: string;
diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts
index dddd03d046227..f21be346eaddd 100644
--- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts
+++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts
@@ -416,7 +416,6 @@ abstract class DashboardScenePageStateManagerBase<T>
if (options.route !== DashboardRoutes.New) {
emitDashboardViewEvent({
- id: dashboard.state.id,
meta: dashboard.state.meta,
uid: dashboard.state.uid,
title: dashboard.state.title,
diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx
index 7409f39edb2f9..c6ce83147bedf 100644
--- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx
+++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx
@@ -156,9 +156,6 @@ export interface DashboardScenePreferences {
}
export interface DashboardSceneState extends SceneObjectState {
- /** @deprecated */
- id?: number | undefined;
-
/** Dashboard-specific preferences **/
preferences?: DashboardScenePreferences;
diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts
index c4bae9cda1e20..d2fdcd6a66d6d 100644
--- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts
+++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts
@@ -56,7 +56,6 @@ import {
AnnoKeyDashboardIsSnapshot,
AnnoKeyEmbedded,
AnnoReloadOnParamsChange,
- DeprecatedInternalId,
} from 'app/features/apiserver/types';
import { type DashboardWithAccessInfo } from 'app/features/dashboard/api/types';
import {
@@ -211,11 +210,8 @@ export function transformSaveModelSchemaV2ToScene(
dashboardProfiler
);
- const deprecatedId = metadata.labels?.[DeprecatedInternalId];
-
const dashboardScene = new DashboardScene(
{
- id: deprecatedId ? parseInt(deprecatedId, 10) : undefined,
preferences: templateLayoutManager
? {
defaultLayoutTemplate: templateLayoutManager,
diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts
index a920321ee7075..612ac23e519fa 100644
--- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts
+++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts
@@ -413,7 +413,6 @@ export function createDashboardSceneFromDashboardModel(
const dashboardScene = new DashboardScene(
{
- id: oldModel.id,
uid,
description: oldModel.description,
editable: oldModel.editable,
diff --git a/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.ts b/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.ts
index 8104f197d7e7b..729b20e1a7e51 100644
--- a/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.ts
+++ b/public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.ts
@@ -39,10 +39,6 @@ export class DashboardModelCompatibilityWrapper {
);
}
- public get id(): number | null {
- return this._scene.state.id ?? null;
- }
-
public get uid() {
return this._scene.state.uid ?? null;
}
diff --git a/public/app/features/dashboard/state/DashboardMigratorSingleVersion.test.ts b/public/app/features/dashboard/state/DashboardMigratorSingleVersion.test.ts
index ac0a93e5cb669..d18d7f78613d6 100644
--- a/public/app/features/dashboard/state/DashboardMigratorSingleVersion.test.ts
+++ b/public/app/features/dashboard/state/DashboardMigratorSingleVersion.test.ts
@@ -134,7 +134,6 @@ describe('Backend / Frontend single version migration result comparison', () =>
}
}
}
- delete frontendMigrationResult.id;
expect(backendMigrationResult).toEqual(frontendMigrationResult);
});
});
diff --git a/public/app/features/dashboard/state/DashboardMigratorToBackend.devDashboards.test.ts b/public/app/features/dashboard/state/DashboardMigratorToBackend.devDashboards.test.ts
index 24aafd733050b..c53fdd7678c63 100644
--- a/public/app/features/dashboard/state/DashboardMigratorToBackend.devDashboards.test.ts
+++ b/public/app/features/dashboard/state/DashboardMigratorToBackend.devDashboards.test.ts
@@ -117,7 +117,6 @@ describe('Dev Dashboard Backend / Frontend result comparison', () => {
// version in the backend is never added because it is returned from the backend as metadata
delete frontendMigrationResult.version;
- delete frontendMigrationResult.id;
expect(backendMigrationResult).toEqual(frontendMigrationResult);
});
diff --git a/public/app/features/dashboard/state/DashboardMigratorToBackend.test.ts b/public/app/features/dashboard/state/DashboardMigratorToBackend.test.ts
index 76736420f542d..8a1eaa96223ad 100644
--- a/public/app/features/dashboard/state/DashboardMigratorToBackend.test.ts
+++ b/public/app/features/dashboard/state/DashboardMigratorToBackend.test.ts
@@ -102,9 +102,6 @@ describe('Backend / Frontend result comparison', () => {
}
}
- // The backend snapshots exclude the internal ids
- delete frontendMigrationResult.id;
-
expect(backendMigrationResult).toEqual(frontendMigrationResult);
});
});
diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts
index 9e327c3568be3..1a2719c24ef42 100644
--- a/public/app/features/dashboard/state/DashboardModel.ts
+++ b/public/app/features/dashboard/state/DashboardModel.ts
@@ -67,9 +67,6 @@ export interface ScopeMeta {
}
export class DashboardModel implements TimeModel {
- /** @deprecated use UID */
- id?: any;
-
// TODO: use proper type and fix all the places where uid is set to null
uid: any;
title: string;
@@ -144,7 +141,6 @@ export class DashboardModel implements TimeModel {
targetSchemaVersion?: number;
}
) {
- this.id = data.id;
this.getVariablesFromState = options?.getVariablesFromState ?? getVariablesByKey;
this.events = new EventBusSrv();
// UID is not there for newly created dashboards
diff --git a/public/app/features/dashboard/state/analyticsProcessor.ts b/public/app/features/dashboard/state/analyticsProcessor.ts
index 9ce0875b12309..657de3adbe736 100644
--- a/public/app/features/dashboard/state/analyticsProcessor.ts
+++ b/public/app/features/dashboard/state/analyticsProcessor.ts
@@ -2,9 +2,8 @@ import { reportMetaAnalytics, MetaAnalyticsEventName, type DashboardViewEventPay
import { type DashboardModel } from './DashboardModel';
-export function emitDashboardViewEvent(dashboard: Pick<DashboardModel, 'title' | 'uid' | 'meta' | 'id'>) {
+export function emitDashboardViewEvent(dashboard: Pick<DashboardModel, 'title' | 'uid' | 'meta'>) {
const eventData: DashboardViewEventPayload = {
- dashboardId: dashboard.id,
dashboardName: dashboard.title,
dashboardUid: dashboard.uid,
folderName: dashboard.meta.folderTitle,
diff --git a/public/app/features/query/state/queryAnalytics.ts b/public/app/features/query/state/queryAnalytics.ts
index c06e5641ac3e5..59c293abced8a 100644
--- a/public/app/features/query/state/queryAnalytics.ts
+++ b/public/app/features/query/state/queryAnalytics.ts
@@ -25,7 +25,6 @@ export function emitDataRequestEvent(datasource: DataSourceApi) {
source: data.request.app,
datasourceName: datasource.name,
datasourceUid: datasource.uid,
- datasourceId: datasource.id, // temporary while we migrate to datasourceUid
datasourceType: datasource.type,
dataSize: 0,
panelId: 0,
@@ -67,7 +66,6 @@ export function emitDataRequestEvent(datasource: DataSourceApi) {
const dashboard = getDashboardSrv().getCurrent();
if (dashboard) {
- eventData.dashboardId = dashboard.id;
eventData.dashboardName = dashboard.title;
eventData.dashboardUid = dashboard.uid;
eventData.folderName = dashboard.meta.folderTitle;