Information Disclosure / Access Control
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]