Information disclosure / ID exposure
Description
This commit hardens exposure of legacy numeric-id data source APIs by default. It renames and repurposes the feature toggle controlling access to APIs that use numeric internal IDs and gates the registration of data source endpoints by id behind a feature flag. The deprecated endpoints that allowed accessing a data source via its numeric id (e.g., GET /api/datasources/{id}, PUT /api/datasources/{id}, and related proxy endpoints) were removed from the OpenAPI schema and are only registered when the new feature flag is enabled. By default, the legacy-id APIs are disabled, reducing information disclosure through internal numeric IDs and potential misuse of legacy endpoints. This fixes a vulnerability where authorized users could enumerate and potentially misuse internal IDs via these endpoints.
Proof of Concept
PoC:\n1) Pre-fix (vulnerable behavior): An operator with read access could enumerate internal data source IDs by probing /api/datasources/{id} for id in [1..N].\n2) Attack: Iterate through numeric IDs to discover existing data sources and read their metadata.\n3) Demo (bash):\n\nTOKEN=$(<token.txt)\nBASE_URL=https://grafana.example/api/datasources\nfor id in {1..1000}; do\n curl -s -H \"Authorization: Bearer ${TOKEN}\" \"${BASE_URL}/${id}\" | jq -r '.id, .name' || true\ndone\n\nExpected result: responses 200 with metadata for existing IDs; 404/403 for non-existent or forbidden IDs. This enables an attacker to map existing data sources and their internal IDs.\n
Commit Details
Author: Gábor Farkas
Date: 2026-03-12 11:40 UTC
Message:
Datasources: Disable deprecated numeric id using APIs by default (#119930)
datasources: disable deprecated APIs by default
Triage Assessment
Vulnerability Type: Information disclosure / ID exposure
Confidence: MEDIUM
Reasoning:
The patch disables the deprecated numeric-id data source API by default by removing reliance on the old disable flag and gating the legacy-id endpoints behind a feature toggle. This reduces exposure to internal numeric IDs and potential information disclosure or misuse via legacy APIs. Code changes rename the toggle and adjust route registration accordingly, indicating a security hardening around exposure of internal IDs.
Verification Assessment
Vulnerability Type: Information disclosure / ID exposure
Confidence: MEDIUM
Affected Versions: < 12.4.0
Code Diff
diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts
index f0340922acd73..2e7894d2f1b2e 100644
--- a/packages/grafana-data/src/types/featureToggles.gen.ts
+++ b/packages/grafana-data/src/types/featureToggles.gen.ts
@@ -314,10 +314,10 @@ export interface FeatureToggles {
*/
datasourceQueryTypes?: boolean;
/**
- * Does not register datasource apis that use the numeric id
+ * Register legacy datasource apis that use the numeric id
* @default false
*/
- datasourceDisableIdApi?: boolean;
+ datasourceLegacyIdApi?: boolean;
/**
* Register /apis/query.grafana.app/ -- will eventually replace /api/ds/query
* @default false
diff --git a/packages/grafana-openapi/src/api/openapi3.json b/packages/grafana-openapi/src/api/openapi3.json
index 68afc3c8b5f41..7555c08249cf2 100644
--- a/packages/grafana-openapi/src/api/openapi3.json
+++ b/packages/grafana-openapi/src/api/openapi3.json
@@ -18111,155 +18111,6 @@
"tags": ["datasources"]
}
},
- "/datasources/proxy/{id}/{datasource_proxy_route}": {
- "delete": {
- "deprecated": true,
- "description": "Proxies all calls to the actual data source.\n\nPlease refer to [updated API](#/datasources/datasourceProxyDELETEByUIDcalls) instead",
- "operationId": "datasourceProxyDELETEcalls",
- "parameters": [
- {
- "in": "path",
- "name": "id",
- "required": true,
- "schema": {
- "type": "string"
- }
- },
- {
- "in": "path",
- "name": "datasource_proxy_route",
- "required": true,
- "schema": {
- "type": "string"
- }
- }
- ],
- "responses": {
- "202": {
- "description": "(empty)"
- },
- "400": {
- "$ref": "#/components/responses/badRequestError"
- },
- "401": {
- "$ref": "#/components/responses/unauthorisedError"
- },
- "403": {
- "$ref": "#/components/responses/forbiddenError"
- },
- "404": {
- "$ref": "#/components/responses/notFoundError"
- },
- "500": {
- "$ref": "#/components/responses/internalServerError"
- }
- },
- "summary": "Data source proxy DELETE calls.",
- "tags": ["datasources"]
- },
- "get": {
- "deprecated": true,
- "description": "Proxies all calls to the actual data source.\n\nPlease refer to [updated API](#/datasources/datasourceProxyGETByUIDcalls) instead",
- "operationId": "datasourceProxyGETcalls",
- "parameters": [
- {
- "in": "path",
- "name": "datasource_proxy_route",
- "required": true,
- "schema": {
- "type": "string"
- }
- },
- {
- "in": "path",
- "name": "id",
- "required": true,
- "schema": {
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "description": "(empty)"
- },
- "400": {
- "$ref": "#/components/responses/badRequestError"
- },
- "401": {
- "$ref": "#/components/responses/unauthorisedError"
- },
- "403": {
- "$ref": "#/components/responses/forbiddenError"
- },
- "404": {
- "$ref": "#/components/responses/notFoundError"
- },
- "500": {
- "$ref": "#/components/responses/internalServerError"
- }
- },
- "summary": "Data source proxy GET calls.",
- "tags": ["datasources"]
- },
- "post": {
- "deprecated": true,
- "description": "Proxies all calls to the actual data source. The data source should support POST methods for the specific path and role as defined\n\nPlease refer to [updated API](#/datasources/datasourceProxyPOSTByUIDcalls) instead",
- "operationId": "datasourceProxyPOSTcalls",
- "parameters": [
- {
- "in": "path",
- "name": "datasource_proxy_route",
- "required": true,
- "schema": {
- "type": "string"
- }
- },
- {
- "in": "path",
- "name": "id",
- "required": true,
- "schema": {
- "type": "string"
- }
- }
- ],
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {}
- }
- },
- "required": true,
- "x-originalParamName": "DatasourceProxyParam"
- },
- "responses": {
- "201": {
- "description": "(empty)"
- },
- "202": {
- "description": "(empty)"
- },
- "400": {
- "$ref": "#/components/responses/badRequestError"
- },
- "401": {
- "$ref": "#/components/responses/unauthorisedError"
- },
- "403": {
- "$ref": "#/components/responses/forbiddenError"
- },
- "404": {
- "$ref": "#/components/responses/notFoundError"
- },
- "500": {
- "$ref": "#/components/responses/internalServerError"
- }
- },
- "summary": "Data source proxy POST calls.",
- "tags": ["datasources"]
- }
- },
"/datasources/uid/{sourceUID}/correlations": {
"get": {
"operationId": "getCorrelationsBySourceUID",
@@ -18947,204 +18798,6 @@
"tags": ["enterprise"]
}
},
- "/datasources/{id}": {
- "delete": {
- "deprecated": true,
- "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `datasources:delete` and scopes: `datasources:*`, `datasources:id:*` and `datasources:id:1` (single data source).\n\nPlease refer to [updated API](#/datasources/deleteDataSourceByUID) instead",
- "operationId": "deleteDataSourceByID",
- "parameters": [
- {
- "in": "path",
- "name": "id",
- "required": true,
- "schema": {
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "$ref": "#/components/responses/okResponse"
- },
- "401": {
- "$ref": "#/components/responses/unauthorisedError"
- },
- "403": {
- "$ref": "#/components/responses/forbiddenError"
- },
- "404": {
- "$ref": "#/components/responses/notFoundError"
- },
- "500": {
- "$ref": "#/components/responses/internalServerError"
- }
- },
- "summary": "Delete an existing data source by id.",
- "tags": ["datasources"]
- },
- "get": {
- "deprecated": true,
- "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `datasources:read` and scopes: `datasources:*`, `datasources:id:*` and `datasources:id:1` (single data source).\n\nPlease refer to [updated API](#/datasources/getDataSourceByUID) instead",
- "operationId": "getDataSourceByID",
- "parameters": [
- {
- "in": "path",
- "name": "id",
- "required": true,
- "schema": {
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "$ref": "#/components/responses/getDataSourceResponse"
- },
- "400": {
- "$ref": "#/components/responses/badRequestError"
- },
- "401": {
- "$ref": "#/components/responses/unauthorisedError"
- },
- "403": {
- "$ref": "#/components/responses/forbiddenError"
- },
- "404": {
- "$ref": "#/components/responses/notFoundError"
- },
- "500": {
- "$ref": "#/components/responses/internalServerError"
- }
- },
- "summary": "Get a single data source by Id.",
- "tags": ["datasources"]
- },
- "put": {
- "deprecated": true,
- "description": "Similar to creating a data source, `password` and `basicAuthPassword` should be defined under\nsecureJsonData in order to be stored securely as an encrypted blob in the database. Then, the\nencrypted fields are listed under secureJsonFields section in the response.\n\nIf you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `datasources:write` and scopes: `datasources:*`, `datasources:id:*` and `datasources:id:1` (single data source).\n\nPlease refer to [updated API](#/datasources/updateDataSourceByUID) instead",
- "operationId": "updateDataSourceByID",
- "parameters": [
- {
- "in": "path",
- "name": "id",
- "required": true,
- "schema": {
- "type": "string"
- }
- }
- ],
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/UpdateDataSourceCommand"
- }
- }
- },
- "required": true,
- "x-originalParamName": "Body"
- },
- "responses": {
- "200": {
- "$ref": "#/components/responses/createOrUpdateDatasourceResponse"
- },
- "401": {
- "$ref": "#/components/responses/unauthorisedError"
- },
- "403": {
- "$ref": "#/components/responses/forbiddenError"
- },
- "500": {
- "$ref": "#/components/responses/internalServerError"
- }
- },
- "summary": "Update an existing data source by its sequential ID.",
- "tags": ["datasources"]
- }
- },
- "/datasources/{id}/health": {
- "get": {
- "deprecated": true,
- "description": "Please refer to [updated API](#/datasources/checkDatasourceHealthWithUID) instead",
- "operationId": "checkDatasourceHealthByID",
- "parameters": [
- {
- "in": "path",
- "name": "id",
- "required": true,
- "schema": {
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "$ref": "#/components/responses/okResponse"
- },
- "400": {
- "$ref": "#/components/responses/badRequestError"
- },
- "401": {
- "$ref": "#/components/responses/unauthorisedError"
- },
- "403": {
- "$ref": "#/components/responses/forbiddenError"
- },
- "500": {
- "$ref": "#/components/responses/internalServerError"
- }
- },
- "summary": "Sends a health check request to the plugin datasource identified by the ID.",
- "tags": ["datasources", "health"]
- }
- },
- "/datasources/{id}/resources/{datasource_proxy_route}": {
- "get": {
- "deprecated": true,
- "description": "Please refer to [updated API](#/datasources/callDatasourceResourceWithUID) instead",
- "operationId": "callDatasourceResourceByID",
- "parameters": [
- {
- "in": "path",
- "name": "datasource_proxy_route",
- "required": true,
- "schema": {
- "type": "string"
- }
- },
- {
- "in": "path",
- "name": "id",
- "required": true,
- "schema": {
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "$ref": "#/components/responses/okResponse"
- },
- "400": {
- "$ref": "#/components/responses/badRequestError"
- },
- "401": {
- "$ref": "#/components/responses/unauthorisedError"
- },
- "403": {
- "$ref": "#/components/responses/forbiddenError"
- },
- "404": {
- "$ref": "#/components/responses/notFoundError"
- },
- "500": {
- "$ref": "#/components/responses/internalServerError"
- }
- },
- "summary": "Fetch data source resources by Id.",
- "tags": ["datasources"]
- }
- },
"/ds/query": {
"post": {
"description": "If you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `datasources:query`.",
diff --git a/pkg/api/api.go b/pkg/api/api.go
index cb48534af0540..46c278177d3e8 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -429,7 +429,7 @@ func (hs *HTTPServer) registerRoutes() {
datasourceRoute.Any("/proxy/uid/:uid/*", requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), authorize(ac.EvalPermission(datasources.ActionQuery)), hs.ProxyDataSourceRequestWithUID)
//nolint:staticcheck // not yet migrated to OpenFeature
- if !hs.Features.IsEnabledGlobally(featuremgmt.FlagDatasourceDisableIdApi) {
+ if hs.Features.IsEnabledGlobally(featuremgmt.FlagDatasourceLegacyIdApi) {
// Deprecated Access by internal ID
datasourceRoute.Get("/:id", authorize(ac.EvalPermission(datasources.ActionRead, idScope)), routing.Wrap(hs.GetDataSourceById))
datasourceRoute.Put("/:id", authorize(ac.EvalPermission(datasources.ActionWrite, idScope)), routing.Wrap(hs.UpdateDataSourceByID))
diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go
index bf205bdbaae8d..7ecfa41fc6312 100644
--- a/pkg/api/dataproxy.go
+++ b/pkg/api/dataproxy.go
@@ -2,8 +2,6 @@ package api
import contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
-// swagger:route GET /datasources/proxy/{id}/{datasource_proxy_route} datasources datasourceProxyGETcalls
-//
// Data source proxy GET calls.
//
// Proxies all calls to the actual data source.
@@ -20,8 +18,6 @@ import contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/mode
// 404: notFoundError
// 500: internalServerError
-// swagger:route POST /datasources/proxy/{id}/{datasource_proxy_route} datasources datasourceProxyPOSTcalls
-//
// Data source proxy POST calls.
//
// Proxies all calls to the actual data source. The data source should support POST methods for the specific path and role as defined
@@ -39,8 +35,6 @@ import contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/mode
// 404: notFoundError
//
... [truncated]