Information Disclosure / Access Control

MEDIUM
grafana/grafana
Commit: 0fedd316ebeb
Affected: <= 12.4.0
2026-05-21 09:43 UTC

Description

Root cause: The datasource proxy previously accepted and threaded the global Grafana settings (*setting.Cfg) through the data source proxy path into the token provider. This could allow the data source proxy to touch and potentially expose sensitive Azure configuration (e.g., client secrets, Azure-specific settings) when building an access token, especially when DataProxyLogging was enabled or an Azure-authenticated route was used. The commit introduces a focused DataSourceProxySettings struct and lazy-loading of Azure settings via a resolver callback, replacing direct usage of *setting.Cfg with this narrower structure. This hardening reduces exposure by restricting what the proxy can see and by deferring access to Azure config until actually needed. It also reduces the surface area through which sensitive config could be disclosed or misused in proxy/auth flows.

Proof of Concept

Proof of concept (high level, executable steps may depend on environment): Reproduction on Grafana 12.4.0 (pre-fix behavior): - Configure Azure authentication in Grafana (for example via grafana.ini or environment) including client_id, tenant_id, and client_secret. - Configure the datasource proxy to use Azure-based token authentication (tokenAuth with type 'azure') and enable DataProxyLogging. - Use the datasource proxy to proxy a request through a route that requires Azure authentication. - Observe logs or request headers where the proxy has already instantiated the Azure token provider using the full global settings (cfg), potentially exposing the Azure client_secret or other Azure config in logs or in memory tied to the proxy process. Potential attack vector: If an attacker can access datasource proxy logs or intercept the proxied request flow, they could observe sensitive Azure configuration values (or tokens generated from them) because the token provider was constructed with the full configuration slate. This constitutes information disclosure via misconfigured proxy/auth flows. Reproduction on patched Grafana (post-fix): - Deploy Grafana with the exact same Azure config, but use the updated code path introduced in this commit: a dedicated DataSourceProxySettings object is passed through, and Azure settings are loaded lazily via a resolver callback only when an Azure-authenticated route is actually matched. - Enable DataProxyLogging to verify that Azure credentials are not eagerly loaded or dumped to logs unless needed. - Trigger an Azure-authenticated route and observe that the proxy only loads the necessary subset of settings and does not expose the full Azure configuration in logs or headers. Expected outcome: Before the fix, sensitive Azure configuration could be exposed via the proxy path or logs. After the fix, Azure settings are loaded lazily and only the restricted DataSourceProxySettings fields are exposed to the proxy logic, mitigating information disclosure risks. Notes: Replace placeholders with actual test Azure settings and proxy routes in a safe, isolated test environment. Do not use production credentials in PoCs.

Commit Details

Author: Ryan McKinley

Date: 2026-05-21 09:09 UTC

Message:

Datasource Proxy: Restrict full access to setting.Cfg (#125180) Plugin Proxy: Narrow data source config to DataSourceProxySettings The data source proxy only needs a handful of fields from setting.Cfg. Introduce a focused DataSourceProxySettings struct that exposes only those fields, load Azure settings lazily via a resolver callback so the proxy avoids touching Azure configuration unless an Azure-authenticated route is matched, and pass the new struct through the datasource proxy service in place of *setting.Cfg.

Triage Assessment

Vulnerability Type: Information Disclosure / Access Control

Confidence: MEDIUM

Reasoning:

The commit introduces a focused DataSourceProxySettings struct and replaces direct usage of the global setting.Cfg. It lazy-loads Azure settings via a resolver callback and restricts data exposed to the datasource proxy, reducing exposure of sensitive configuration and preventing unnecessary access to Azure settings. This hardening mitigates potential information disclosure and misconfiguration risks in proxy/auth flows.

Verification Assessment

Vulnerability Type: Information Disclosure / Access Control

Confidence: MEDIUM

Affected Versions: <= 12.4.0

Code Diff

diff --git a/pkg/api/pluginproxy/ds_auth_provider.go b/pkg/api/pluginproxy/ds_auth_provider.go index 7887f6f4196be..fbbc9e6ad1d06 100644 --- a/pkg/api/pluginproxy/ds_auth_provider.go +++ b/pkg/api/pluginproxy/ds_auth_provider.go @@ -9,7 +9,6 @@ import ( "time" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -23,7 +22,7 @@ type DSInfo struct { // ApplyRoute should use the plugin route data to set auth headers and custom headers. func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route *plugins.Route, - ds DSInfo, cfg *setting.Cfg) { + ds DSInfo, settings *DataSourceProxySettings) { proxyPath = strings.TrimPrefix(proxyPath, route.Path) data := templateData{ URL: ds.URL, @@ -70,7 +69,7 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route ctxLogger.Error("Failed to set plugin route body content", "error", err) } - if tokenProvider, err := getTokenProvider(ctx, cfg, ds, route, data); err != nil { + if tokenProvider, err := getTokenProvider(ctx, settings, ds, route, data); err != nil { ctxLogger.Error("Failed to resolve auth token provider", "error", err) } else if tokenProvider != nil { if token, err := tokenProvider.GetAccessToken(); err != nil { @@ -80,12 +79,12 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route } } - if cfg.DataProxyLogging { + if settings.DataProxyLogging { ctxLogger.Debug("Requesting", "url", req.URL.String()) } } -func getTokenProvider(ctx context.Context, cfg *setting.Cfg, ds DSInfo, pluginRoute *plugins.Route, +func getTokenProvider(ctx context.Context, settings *DataSourceProxySettings, ds DSInfo, pluginRoute *plugins.Route, data templateData) (accessTokenProvider, error) { authType := pluginRoute.AuthType @@ -108,7 +107,7 @@ func getTokenProvider(ctx context.Context, cfg *setting.Cfg, ds DSInfo, pluginRo if tokenAuth == nil { return nil, fmt.Errorf("'tokenAuth' not configured for authentication type '%s'", authType) } - return newAzureAccessTokenProvider(ctx, cfg, tokenAuth) + return newAzureAccessTokenProvider(ctx, settings, tokenAuth) case "gce": if jwtTokenAuth == nil { diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go index 56982f00b912f..fdc77224010de 100644 --- a/pkg/api/pluginproxy/ds_proxy.go +++ b/pkg/api/pluginproxy/ds_proxy.go @@ -24,7 +24,6 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/oauthtoken" pluginac "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol" - "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util/proxyutil" ) @@ -47,7 +46,7 @@ type DataSourceProxy struct { proxyPath string matchedRoute *plugins.Route pluginRoutes []*plugins.Route - cfg *setting.Cfg + settings *DataSourceProxySettings clientProvider httpclient.Provider oAuthTokenService oauthtoken.OAuthTokenService dataSourcesService datasources.DataSourceService @@ -61,7 +60,7 @@ type httpClient interface { // NewDataSourceProxy creates a new Datasource proxy func NewDataSourceProxy(ds *datasources.DataSource, pluginRoutes []*plugins.Route, ctx *contextmodel.ReqContext, - proxyPath string, cfg *setting.Cfg, clientProvider httpclient.Provider, + proxyPath string, settings *DataSourceProxySettings, clientProvider httpclient.Provider, oAuthTokenService oauthtoken.OAuthTokenService, dsService datasources.DataSourceService, tracer tracing.Tracer, features featuremgmt.FeatureToggles, ) (*DataSourceProxy, error) { @@ -76,7 +75,7 @@ func NewDataSourceProxy(ds *datasources.DataSource, pluginRoutes []*plugins.Rout ctx: ctx, proxyPath: proxyPath, targetUrl: targetURL, - cfg: cfg, + settings: settings, clientProvider: clientProvider, oAuthTokenService: oAuthTokenService, dataSourcesService: dsService, @@ -237,11 +236,11 @@ func (proxy *DataSourceProxy) director(req *http.Request) { req.Header.Set("Authorization", dsAuth) } - proxyutil.ApplyUserHeader(proxy.cfg.SendUserHeader, req, proxy.ctx.SignedInUser) + proxyutil.ApplyUserHeader(proxy.settings.SendUserHeader, req, proxy.ctx.SignedInUser) - proxyutil.ClearCookieHeader(req, proxy.ds.AllowedCookies(), []string{proxy.cfg.LoginCookieName}) - ua := proxy.cfg.DataProxyUserAgent - if proxy.cfg.DataProxyForwardUserAgent { + proxyutil.ClearCookieHeader(req, proxy.ds.AllowedCookies(), []string{proxy.settings.LoginCookieName}) + ua := proxy.settings.DataProxyUserAgent + if proxy.settings.DataProxyForwardUserAgent { if originalUA := req.Header.Get("User-Agent"); originalUA != "" { if len(originalUA) > maxForwardedUserAgentLen { originalUA = originalUA[:maxForwardedUserAgentLen] @@ -277,7 +276,7 @@ func (proxy *DataSourceProxy) director(req *http.Request) { Updated: proxy.ds.Updated, JSONData: jsonData, DecryptedSecureJSONData: decryptedValues, - }, proxy.cfg) + }, proxy.settings) } if proxy.oAuthTokenService.IsOAuthPassThruEnabled(proxy.ds) { @@ -384,7 +383,7 @@ func (proxy *DataSourceProxy) hasAccessToRoute(route *plugins.Route) bool { } func (proxy *DataSourceProxy) logRequest() { - if !proxy.cfg.DataProxyLogging { + if !proxy.settings.DataProxyLogging { return } @@ -417,8 +416,8 @@ func (proxy *DataSourceProxy) logRequest() { } func (proxy *DataSourceProxy) checkWhiteList() bool { - if proxy.targetUrl.Host != "" && len(proxy.cfg.DataProxyWhiteList) > 0 { - if _, exists := proxy.cfg.DataProxyWhiteList[proxy.targetUrl.Host]; !exists { + if proxy.targetUrl.Host != "" && len(proxy.settings.DataProxyWhiteList) > 0 { + if _, exists := proxy.settings.DataProxyWhiteList[proxy.targetUrl.Host]; !exists { proxy.ctx.JsonApiErr(403, "Data proxy hostname and ip are not included in whitelist", nil) return false } diff --git a/pkg/api/pluginproxy/ds_proxy_test.go b/pkg/api/pluginproxy/ds_proxy_test.go index 6034ccb51055a..20114d815d669 100644 --- a/pkg/api/pluginproxy/ds_proxy_test.go +++ b/pkg/api/pluginproxy/ds_proxy_test.go @@ -163,7 +163,7 @@ func TestIntegrationDataSourceProxy_routeRule(t *testing.T) { proxy, err := setupDSProxyTest(t, ctx, ds, routes, "api/v4/some/method") require.NoError(t, err) proxy.matchedRoute = routes[0] - ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.matchedRoute, dsInfo, proxy.cfg) + ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.matchedRoute, dsInfo, proxy.settings) assert.Equal(t, "https://www.google.com/some/method", req.URL.String()) assert.Equal(t, "my secret 123", req.Header.Get("x-header")) @@ -174,7 +174,7 @@ func TestIntegrationDataSourceProxy_routeRule(t *testing.T) { proxy, err := setupDSProxyTest(t, ctx, ds, routes, "api/common/some/method") require.NoError(t, err) proxy.matchedRoute = routes[3] - ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.matchedRoute, dsInfo, proxy.cfg) + ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.matchedRoute, dsInfo, proxy.settings) assert.Equal(t, "https://dynamic.grafana.com/some/method?apiKey=123", req.URL.String()) assert.Equal(t, "my secret 123", req.Header.Get("x-header")) @@ -185,7 +185,7 @@ func TestIntegrationDataSourceProxy_routeRule(t *testing.T) { proxy, err := setupDSProxyTest(t, ctx, ds, routes, "") require.NoError(t, err) proxy.matchedRoute = routes[4] - ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.matchedRoute, dsInfo, proxy.cfg) + ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.matchedRoute, dsInfo, proxy.settings) assert.Equal(t, "http://localhost/asd", req.URL.String()) }) @@ -214,7 +214,7 @@ func TestIntegrationDataSourceProxy_routeRule(t *testing.T) { }, URL: "https://dynamic.grafana.com", } - ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.matchedRoute, dsInfo, proxy.cfg) + ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.matchedRoute, dsInfo, proxy.settings) assert.Equal(t, "https://dynamic.grafana.com/some/method?apiKey=123", req.URL.String()) assert.Equal(t, "my secret 123", req.Header.Get("x-header")) @@ -225,7 +225,7 @@ func TestIntegrationDataSourceProxy_routeRule(t *testing.T) { proxy, err := setupDSProxyTest(t, ctx, ds, routes, "api/body") require.NoError(t, err) proxy.matchedRoute = routes[5] - ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.matchedRoute, dsInfo, proxy.cfg) + ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.matchedRoute, dsInfo, proxy.settings) content, err := io.ReadAll(req.Body) require.NoError(t, err) @@ -237,7 +237,7 @@ func TestIntegrationDataSourceProxy_routeRule(t *testing.T) { proxy, err := setupDSProxyTest(t, ctx, ds, routes, "mypath/some-route/") require.NoError(t, err) proxy.matchedRoute = routes[6] - ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.matchedRoute, dsInfo, proxy.cfg) + ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.matchedRoute, dsInfo, proxy.settings) assert.Equal(t, "https://example.com/api/v1/some-route/", req.URL.String()) }) @@ -247,7 +247,7 @@ func TestIntegrationDataSourceProxy_routeRule(t *testing.T) { proxy, err := setupDSProxyTest(t, ctx, ds, routes, "/our%20devices") require.NoError(t, err) proxy.matchedRoute = routes[9] - ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.matchedRoute, dsInfo, proxy.cfg) + ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.matchedRoute, dsInfo, proxy.settings) assert.Equal(t, "http://encoded.com/our%20devices", req.URL.String()) }) @@ -402,7 +402,7 @@ func TestIntegrationDataSourceProxy_routeRule(t *testing.T) { proxy, err := setupDSProxyTest(t, ctx, ds, routes, "pathwithtoken1") require.NoError(t, err) - ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[0], dsInfo, proxy.cfg) + ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[0], dsInfo, proxy.settings) authorizationHeaderCall1 = req.Header.Get("Authorization") assert.Equal(t, "https://api.nr1.io/some/path", req.URL.String()) @@ -419,7 +419,7 @@ func TestIntegrationDataSourceProxy_routeRule(t *testing.T) { proxy, err := setupDSProxyTest(t, ctx, ds, routes, "pathwithtoken2") require.NoError(t, err) - ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[1], dsInfo, proxy.cfg) + ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[1], dsInfo, proxy.settings) authorizationHeaderCall2 = req.Header.Get("Authorization") @@ -436,7 +436,7 @@ func TestIntegrationDataSourceProxy_routeRule(t *testing.T) { proxy, err := setupDSProxyTest(t, ctx, ds, routes, "pathwithtoken1") require.NoError(t, err) - ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[0], dsInfo, proxy.cfg) + ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[0], dsInfo, proxy.settings) authorizationHeaderCall3 := req.Header.Get("Authorization") assert.Equal(t, "https://api.nr1.io/some/path", req.URL.String()) @@ -455,7 +455,7 @@ func TestIntegrationDataSourceProxy_routeRule(t *testing.T) { ctx := &contextmodel.ReqContext{} proxy, err := setupDSProxyTest(t, ctx, ds, routes, "/render", func(proxy *DataSourceProxy) { - proxy.cfg = &setting.Cfg{BuildVersion: "5.3.0"} + proxy.settings = &DataSourceProxySettings{} }) require.NoError(t, err) req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) @@ -619,7 +619,7 @@ func TestIntegrationDataSourceProxy_routeRule(t *testing.T) { UserID: 1, }, }, - &setting.Cfg{SendUserHeader: true}, + &DataSourceProxySettings{SendUserHeader: true}, ) assert.Equal(t, "test_user", req.Header.Get("X-Grafana-User")) }) @@ -632,7 +632,7 @@ func TestIntegrationDataSourceProxy_routeRule(t *testing.T) { Login: "test_user", }, }, - &setting.Cfg{SendUserHeader: false}, + &DataSourceProxySettings{SendUserHeader: false}, ) // Get will return empty string even if header is not set assert.Empty(t, req.Header.Get("X-Grafana-User")) @@ -644,7 +644,7 @@ func TestIntegrationDataSourceProxy_routeRule(t *testing.T) { &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{IsAnonymous: true}, }, - &setting.Cfg{SendUserHeader: true}, + &DataSourceProxySettings{SendUserHeader: true}, ) // Get will return empty string even if header is not set assert.Empty(t, req.Header.Get("X-Grafana-User")) @@ -770,8 +770,7 @@ func TestDataSourceProxy_userAgentHeader(t *testing.T) { t.Run("When DataProxyForwardUserAgent config is disabled", func(t *testing.T) { ctx := &contextmodel.ReqContext{} proxy, err := setupDSProxyTest(t, ctx, ds, routes, "/render", func(p *DataSourceProxy) { - p.cfg = &setting.Cfg{ - BuildVersion: "5.3.0", + p.settings = &DataSourceProxySettings{ DataProxyUserAgent: "Grafana/5.3.0", DataProxyForwardUserAgent: false, } @@ -790,8 +789,7 @@ func TestDataSourceProxy_userAgentHeader(t *testing.T) { t.Run("When DataProxyForwardUserAgent config is enabled", func(t *testing.T) { ctx := &contextmodel.ReqContext{} proxy, err := setupDSProxyTest(t, ctx, ds, routes, "/render", func(p *DataSourceProxy) { - p.cfg = &setting.Cfg{ - BuildVersion: "5.3.0", + p.settings = &DataSourceProxySettings{ DataProxyUserAgent: "Grafana/5.3.0", DataProxyForwardUserAgent: true, } @@ -810,8 +808,7 @@ func TestDataSourceProxy_userAgentHeader(t *testing.T) { t.Run("When DataProxyForwardUserAgent config is enabled but the client User-Agent is empty", func(t *testing.T) { ctx := &contextmodel.ReqContext{} proxy, err := setupDSProxyTest(t, ctx, ds, routes, "/render", func(p *DataSourceProxy) { - p.cfg = &setting.Cfg{ - BuildVersion: "5.3.0", + p.settings = &DataSourceProxySettings{ DataProxyUserAgent: "Grafana/5.3.0", DataProxyForwardUserAgent: true, } @@ -830,8 +827,7 @@ func TestDataSourceProxy_userAgentHeader(t *testing.T) { t.Run("When DataProxyForwardUserAgent config is enabled with a custom DataProxyUserAgent", func(t *testing.T) { ctx := &contextmodel.ReqContext{} proxy, err := setupDSProxyTest(t, ctx, ds, routes, "/render", func(p *DataSourceProxy) { - p.cfg = &setting.Cfg{ - BuildVersion: "5.3.0", + p.settings = &DataSourceProxySettings{ DataProxyUserAgent: "MyCorp/1.0", DataProxyForwardUserAgent: true, } @@ -850,8 +846,7 @@ func Tes ... [truncated]
← Back to Alerts View on GitHub →