Grafana data source and plugin proxy endpoints leaking authentication tokens to some destination plugins
Description
Grafana is an open source observability and data visualization platform. Versions of Grafana for endpoints prior to 9.1.8 and 8.5.14 could leak authentication tokens to some destination plugins under some conditions. The vulnerability impacts data source and plugin proxy endpoints with authentication tokens. The destination plugin could receive a user's Grafana authentication token. Versions 9.1.8 and 8.5.14 contain a patch for this issue. As a workaround, do not use API keys, JWT authentication, or any HTTP Header based authentication.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/grafana/grafanaGo | >= 9.0.0, < 9.1.8 | 9.1.8 |
github.com/grafana/grafanaGo | >= 7.0.0, < 8.5.14 | 8.5.14 |
Affected products
1Patches
24dd56e4dabceSecurity: Make proxy endpoints not leak sensitive HTTP headers
9 files changed · +102 −2
pkg/api/plugins.go+9 −0 modified@@ -24,6 +24,7 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin" "github.com/grafana/grafana/pkg/plugins/manager/installer" + "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/pluginsettings" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util/errutil" @@ -546,6 +547,14 @@ func (hs *HTTPServer) makePluginResourceRequest(w http.ResponseWriter, req *http hs.log.Warn("failed to to unpack JSONData in datasource instance settings", "err", err) } } + + list := contexthandler.AuthHTTPHeaderListFromContext(req.Context()) + if list != nil { + for _, name := range list.Items { + req.Header.Del(name) + } + } + proxyutil.ClearCookieHeader(req, keepCookieModel.KeepCookies) proxyutil.PrepareProxyRequest(req)
pkg/api/plugins_test.go+8 −0 modified@@ -20,6 +20,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web/webtest" ) @@ -275,6 +276,12 @@ func TestMakePluginResourceRequest(t *testing.T) { pluginClient: pluginClient, } req := httptest.NewRequest(http.MethodGet, "/", nil) + + const customHeader = "X-CUSTOM" + req.Header.Set(customHeader, "val") + ctx := contexthandler.WithAuthHTTPHeader(req.Context(), customHeader) + req = req.WithContext(ctx) + resp := httptest.NewRecorder() pCtx := backend.PluginContext{} err := hs.makePluginResourceRequest(resp, req, pCtx) @@ -287,6 +294,7 @@ func TestMakePluginResourceRequest(t *testing.T) { } require.Equal(t, "sandbox", resp.Header().Get("Content-Security-Policy")) + require.Empty(t, req.Header.Get(customHeader)) } func callGetPluginAsset(sc *scenarioContext) {
pkg/middleware/middleware_basic_auth_test.go+6 −0 modified@@ -36,6 +36,9 @@ func TestMiddlewareBasicAuth(t *testing.T) { assert.True(t, sc.context.IsSignedIn) assert.Equal(t, orgID, sc.context.OrgId) assert.Equal(t, models.ROLE_EDITOR, sc.context.OrgRole) + list := contexthandler.AuthHTTPHeaderListFromContext(sc.context.Req.Context()) + require.NotNil(t, list) + require.EqualValues(t, []string{"Authorization"}, list.Items) }, configure) middlewareScenario(t, "Handle auth", func(t *testing.T, sc *scenarioContext) { @@ -69,6 +72,9 @@ func TestMiddlewareBasicAuth(t *testing.T) { assert.True(t, sc.context.IsSignedIn) assert.Equal(t, id, sc.context.UserId) + list := contexthandler.AuthHTTPHeaderListFromContext(sc.context.Req.Context()) + require.NotNil(t, list) + require.EqualValues(t, []string{"Authorization"}, list.Items) }, configure) middlewareScenario(t, "Should return error if user is not found", func(t *testing.T, sc *scenarioContext) {
pkg/middleware/middleware_jwt_auth_test.go+4 −0 modified@@ -6,6 +6,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/contexthandler" @@ -54,6 +55,9 @@ func TestMiddlewareJWTAuth(t *testing.T) { assert.Equal(t, orgID, sc.context.OrgId) assert.Equal(t, id, sc.context.UserId) assert.Equal(t, myUsername, sc.context.Login) + list := contexthandler.AuthHTTPHeaderListFromContext(sc.context.Req.Context()) + require.NotNil(t, list) + require.EqualValues(t, []string{sc.cfg.JWTAuthHeaderName}, list.Items) }, configure, configureUsernameClaim) middlewareScenario(t, "Valid token with valid email claim", func(t *testing.T, sc *scenarioContext) {
pkg/middleware/middleware_test.go+5 −0 modified@@ -395,6 +395,11 @@ func TestMiddlewareContext(t *testing.T) { assert.True(t, sc.context.IsSignedIn) assert.Equal(t, userID, sc.context.UserId) assert.Equal(t, orgID, sc.context.OrgId) + list := contexthandler.AuthHTTPHeaderListFromContext(sc.context.Req.Context()) + require.NotNil(t, list) + require.Contains(t, list.Items, sc.cfg.AuthProxyHeaderName) + require.Contains(t, list.Items, "X-WEBAUTH-GROUPS") + require.Contains(t, list.Items, "X-WEBAUTH-ROLE") }, func(cfg *setting.Cfg) { configure(cfg) cfg.LDAPEnabled = false
pkg/services/contexthandler/auth_jwt.go+3 −0 modified@@ -93,6 +93,9 @@ func (h *ContextHandler) initContextWithJWT(ctx *models.ReqContext, orgId int64) return true } + newCtx := WithAuthHTTPHeader(ctx.Req.Context(), h.Cfg.JWTAuthHeaderName) + *ctx.Req = *ctx.Req.WithContext(newCtx) + ctx.SignedInUser = query.Result ctx.IsSignedIn = true
pkg/services/contexthandler/contexthandler.go+52 −2 modified@@ -211,6 +211,9 @@ func (h *ContextHandler) initContextWithAPIKey(reqContext *models.ReqContext) bo return true } + ctx := WithAuthHTTPHeader(reqContext.Req.Context(), "Authorization") + *reqContext.Req = *reqContext.Req.WithContext(ctx) + // fetch key keyQuery := models.GetApiKeyByNameQuery{KeyName: decoded.Name, OrgId: decoded.OrgId} if err := h.SQLStore.GetApiKeyByName(reqContext.Req.Context(), &keyQuery); err != nil { @@ -287,7 +290,7 @@ func (h *ContextHandler) initContextWithBasicAuth(reqContext *models.ReqContext, return false } - ctx, span := h.tracer.Start(reqContext.Req.Context(), "initContextWithBasicAuth") + _, span := h.tracer.Start(reqContext.Req.Context(), "initContextWithBasicAuth") defer span.End() username, password, err := util.DecodeBasicAuthHeader(header) @@ -296,12 +299,15 @@ func (h *ContextHandler) initContextWithBasicAuth(reqContext *models.ReqContext, return true } + ctx := WithAuthHTTPHeader(reqContext.Req.Context(), "Authorization") + *reqContext.Req = *reqContext.Req.WithContext(ctx) + authQuery := models.LoginUserQuery{ Username: username, Password: password, Cfg: h.Cfg, } - if err := h.authenticator.AuthenticateUser(reqContext.Req.Context(), &authQuery); err != nil { + if err := h.authenticator.AuthenticateUser(ctx, &authQuery); err != nil { reqContext.Logger.Debug( "Failed to authorize the user", "username", username, @@ -523,6 +529,15 @@ func (h *ContextHandler) initContextWithAuthProxy(reqContext *models.ReqContext, logger.Debug("Successfully got user info", "userID", user.UserId, "username", user.Login) + ctx := WithAuthHTTPHeader(reqContext.Req.Context(), h.Cfg.AuthProxyHeaderName) + for _, header := range h.Cfg.AuthProxyHeaders { + if header != "" { + ctx = WithAuthHTTPHeader(ctx, header) + } + } + + *reqContext.Req = *reqContext.Req.WithContext(ctx) + // Add user info to context reqContext.SignedInUser = user reqContext.IsSignedIn = true @@ -542,3 +557,38 @@ func (h *ContextHandler) initContextWithAuthProxy(reqContext *models.ReqContext, return true } + +type authHTTPHeaderListContextKey struct{} + +var authHTTPHeaderListKey = authHTTPHeaderListContextKey{} + +// AuthHTTPHeaderList used to record HTTP headers that being when verifying authentication +// of an incoming HTTP request. +type AuthHTTPHeaderList struct { + Items []string +} + +// WithAuthHTTPHeader returns a copy of parent in which the named HTTP header will be included +// and later retrievable by AuthHTTPHeaderListFromContext. +func WithAuthHTTPHeader(parent context.Context, name string) context.Context { + list := AuthHTTPHeaderListFromContext(parent) + + if list == nil { + list = &AuthHTTPHeaderList{ + Items: []string{}, + } + } + + list.Items = append(list.Items, name) + + return context.WithValue(parent, authHTTPHeaderListKey, list) +} + +// AuthHTTPHeaderListFromContext returns the AuthHTTPHeaderList in a context.Context, if any, +// and will include any HTTP headers used when verifying authentication of an incoming HTTP request. +func AuthHTTPHeaderListFromContext(c context.Context) *AuthHTTPHeaderList { + if list, ok := c.Value(authHTTPHeaderListKey).(*AuthHTTPHeaderList); ok { + return list + } + return nil +}
pkg/util/proxyutil/reverse_proxy.go+8 −0 modified@@ -10,6 +10,7 @@ import ( "time" glog "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/contexthandler" ) // StatusClientClosedRequest A non-standard status code introduced by nginx @@ -66,6 +67,13 @@ func NewReverseProxy(logger glog.Logger, director func(*http.Request), opts ...R // wrapDirector wraps a director and adds additional functionality. func wrapDirector(d func(*http.Request)) func(req *http.Request) { return func(req *http.Request) { + list := contexthandler.AuthHTTPHeaderListFromContext(req.Context()) + if list != nil { + for _, name := range list.Items { + req.Header.Del(name) + } + } + d(req) PrepareProxyRequest(req)
pkg/util/proxyutil/reverse_proxy_test.go+7 −0 modified@@ -9,6 +9,7 @@ import ( "time" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/stretchr/testify/require" ) @@ -30,6 +31,11 @@ func TestReverseProxy(t *testing.T) { req.Header.Set("Referer", "https://test.com/api") req.RemoteAddr = "10.0.0.1" + const customHeader = "X-CUSTOM" + req.Header.Set(customHeader, "val") + ctx := contexthandler.WithAuthHTTPHeader(req.Context(), customHeader) + req = req.WithContext(ctx) + rp := NewReverseProxy(log.New("test"), func(req *http.Request) { req.Header.Set("X-KEY", "value") }) @@ -49,6 +55,7 @@ func TestReverseProxy(t *testing.T) { require.Empty(t, resp.Cookies()) require.Equal(t, "sandbox", resp.Header.Get("Content-Security-Policy")) require.NoError(t, resp.Body.Close()) + require.Empty(t, actualReq.Header.Get(customHeader)) }) t.Run("When proxying a request using WithModifyResponse should call it before default ModifyResponse func", func(t *testing.T) {
9da278c044baPlugins: Make proxy endpoints not leak sensitive HTTP headers
9 files changed · +102 −2
pkg/api/plugin_resource.go+9 −0 modified@@ -15,6 +15,7 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins/backendplugin" + "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/util/proxyutil" "github.com/grafana/grafana/pkg/web" @@ -118,6 +119,14 @@ func (hs *HTTPServer) makePluginResourceRequest(w http.ResponseWriter, req *http hs.log.Warn("failed to unpack JSONData in datasource instance settings", "err", err) } } + + list := contexthandler.AuthHTTPHeaderListFromContext(req.Context()) + if list != nil { + for _, name := range list.Items { + req.Header.Del(name) + } + } + proxyutil.ClearCookieHeader(req, keepCookieModel.KeepCookies) proxyutil.PrepareProxyRequest(req)
pkg/api/plugins_test.go+8 −0 modified@@ -21,6 +21,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log/logtest" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web/webtest" @@ -271,6 +272,12 @@ func TestMakePluginResourceRequest(t *testing.T) { pluginClient: pluginClient, } req := httptest.NewRequest(http.MethodGet, "/", nil) + + const customHeader = "X-CUSTOM" + req.Header.Set(customHeader, "val") + ctx := contexthandler.WithAuthHTTPHeader(req.Context(), customHeader) + req = req.WithContext(ctx) + resp := httptest.NewRecorder() pCtx := backend.PluginContext{} err := hs.makePluginResourceRequest(resp, req, pCtx) @@ -283,6 +290,7 @@ func TestMakePluginResourceRequest(t *testing.T) { } require.Equal(t, "sandbox", resp.Header().Get("Content-Security-Policy")) + require.Empty(t, req.Header.Get(customHeader)) } func callGetPluginAsset(sc *scenarioContext) {
pkg/middleware/middleware_basic_auth_test.go+6 −0 modified@@ -37,6 +37,9 @@ func TestMiddlewareBasicAuth(t *testing.T) { assert.True(t, sc.context.IsSignedIn) assert.Equal(t, orgID, sc.context.OrgId) assert.Equal(t, models.ROLE_EDITOR, sc.context.OrgRole) + list := contexthandler.AuthHTTPHeaderListFromContext(sc.context.Req.Context()) + require.NotNil(t, list) + require.EqualValues(t, []string{"Authorization"}, list.Items) }, configure) middlewareScenario(t, "Handle auth", func(t *testing.T, sc *scenarioContext) { @@ -70,6 +73,9 @@ func TestMiddlewareBasicAuth(t *testing.T) { assert.True(t, sc.context.IsSignedIn) assert.Equal(t, id, sc.context.UserId) + list := contexthandler.AuthHTTPHeaderListFromContext(sc.context.Req.Context()) + require.NotNil(t, list) + require.EqualValues(t, []string{"Authorization"}, list.Items) }, configure) middlewareScenario(t, "Should return error if user is not found", func(t *testing.T, sc *scenarioContext) {
pkg/middleware/middleware_jwt_auth_test.go+4 −0 modified@@ -6,6 +6,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/contexthandler" @@ -55,6 +56,9 @@ func TestMiddlewareJWTAuth(t *testing.T) { assert.Equal(t, orgID, sc.context.OrgId) assert.Equal(t, id, sc.context.UserId) assert.Equal(t, myUsername, sc.context.Login) + list := contexthandler.AuthHTTPHeaderListFromContext(sc.context.Req.Context()) + require.NotNil(t, list) + require.EqualValues(t, []string{sc.cfg.JWTAuthHeaderName}, list.Items) }, configure, configureUsernameClaim) middlewareScenario(t, "Valid token with valid email claim", func(t *testing.T, sc *scenarioContext) {
pkg/middleware/middleware_test.go+5 −0 modified@@ -396,6 +396,11 @@ func TestMiddlewareContext(t *testing.T) { assert.True(t, sc.context.IsSignedIn) assert.Equal(t, userID, sc.context.UserId) assert.Equal(t, orgID, sc.context.OrgId) + list := contexthandler.AuthHTTPHeaderListFromContext(sc.context.Req.Context()) + require.NotNil(t, list) + require.Contains(t, list.Items, sc.cfg.AuthProxyHeaderName) + require.Contains(t, list.Items, "X-WEBAUTH-GROUPS") + require.Contains(t, list.Items, "X-WEBAUTH-ROLE") }, func(cfg *setting.Cfg) { configure(cfg) cfg.LDAPEnabled = false
pkg/services/contexthandler/auth_jwt.go+3 −0 modified@@ -99,6 +99,9 @@ func (h *ContextHandler) initContextWithJWT(ctx *models.ReqContext, orgId int64) return true } + newCtx := WithAuthHTTPHeader(ctx.Req.Context(), h.Cfg.JWTAuthHeaderName) + *ctx.Req = *ctx.Req.WithContext(newCtx) + ctx.SignedInUser = query.Result ctx.IsSignedIn = true
pkg/services/contexthandler/contexthandler.go+52 −2 modified@@ -244,6 +244,9 @@ func (h *ContextHandler) initContextWithAPIKey(reqContext *models.ReqContext) bo _, span := h.tracer.Start(reqContext.Req.Context(), "initContextWithAPIKey") defer span.End() + ctx := WithAuthHTTPHeader(reqContext.Req.Context(), "Authorization") + *reqContext.Req = *reqContext.Req.WithContext(ctx) + var ( apikey *models.ApiKey errKey error @@ -326,7 +329,7 @@ func (h *ContextHandler) initContextWithBasicAuth(reqContext *models.ReqContext, return false } - ctx, span := h.tracer.Start(reqContext.Req.Context(), "initContextWithBasicAuth") + _, span := h.tracer.Start(reqContext.Req.Context(), "initContextWithBasicAuth") defer span.End() username, password, err := util.DecodeBasicAuthHeader(header) @@ -335,12 +338,15 @@ func (h *ContextHandler) initContextWithBasicAuth(reqContext *models.ReqContext, return true } + ctx := WithAuthHTTPHeader(reqContext.Req.Context(), "Authorization") + *reqContext.Req = *reqContext.Req.WithContext(ctx) + authQuery := models.LoginUserQuery{ Username: username, Password: password, Cfg: h.Cfg, } - if err := h.authenticator.AuthenticateUser(reqContext.Req.Context(), &authQuery); err != nil { + if err := h.authenticator.AuthenticateUser(ctx, &authQuery); err != nil { reqContext.Logger.Debug( "Failed to authorize the user", "username", username, @@ -571,6 +577,15 @@ func (h *ContextHandler) initContextWithAuthProxy(reqContext *models.ReqContext, logger.Debug("Successfully got user info", "userID", user.UserId, "username", user.Login) + ctx := WithAuthHTTPHeader(reqContext.Req.Context(), h.Cfg.AuthProxyHeaderName) + for _, header := range h.Cfg.AuthProxyHeaders { + if header != "" { + ctx = WithAuthHTTPHeader(ctx, header) + } + } + + *reqContext.Req = *reqContext.Req.WithContext(ctx) + // Add user info to context reqContext.SignedInUser = user reqContext.IsSignedIn = true @@ -590,3 +605,38 @@ func (h *ContextHandler) initContextWithAuthProxy(reqContext *models.ReqContext, return true } + +type authHTTPHeaderListContextKey struct{} + +var authHTTPHeaderListKey = authHTTPHeaderListContextKey{} + +// AuthHTTPHeaderList used to record HTTP headers that being when verifying authentication +// of an incoming HTTP request. +type AuthHTTPHeaderList struct { + Items []string +} + +// WithAuthHTTPHeader returns a copy of parent in which the named HTTP header will be included +// and later retrievable by AuthHTTPHeaderListFromContext. +func WithAuthHTTPHeader(parent context.Context, name string) context.Context { + list := AuthHTTPHeaderListFromContext(parent) + + if list == nil { + list = &AuthHTTPHeaderList{ + Items: []string{}, + } + } + + list.Items = append(list.Items, name) + + return context.WithValue(parent, authHTTPHeaderListKey, list) +} + +// AuthHTTPHeaderListFromContext returns the AuthHTTPHeaderList in a context.Context, if any, +// and will include any HTTP headers used when verifying authentication of an incoming HTTP request. +func AuthHTTPHeaderListFromContext(c context.Context) *AuthHTTPHeaderList { + if list, ok := c.Value(authHTTPHeaderListKey).(*AuthHTTPHeaderList); ok { + return list + } + return nil +}
pkg/util/proxyutil/reverse_proxy.go+8 −0 modified@@ -10,6 +10,7 @@ import ( "time" glog "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/contexthandler" ) // StatusClientClosedRequest A non-standard status code introduced by nginx @@ -66,6 +67,13 @@ func NewReverseProxy(logger glog.Logger, director func(*http.Request), opts ...R // wrapDirector wraps a director and adds additional functionality. func wrapDirector(d func(*http.Request)) func(req *http.Request) { return func(req *http.Request) { + list := contexthandler.AuthHTTPHeaderListFromContext(req.Context()) + if list != nil { + for _, name := range list.Items { + req.Header.Del(name) + } + } + d(req) PrepareProxyRequest(req)
pkg/util/proxyutil/reverse_proxy_test.go+7 −0 modified@@ -9,6 +9,7 @@ import ( "time" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/stretchr/testify/require" ) @@ -30,6 +31,11 @@ func TestReverseProxy(t *testing.T) { req.Header.Set("Referer", "https://test.com/api") req.RemoteAddr = "10.0.0.1" + const customHeader = "X-CUSTOM" + req.Header.Set(customHeader, "val") + ctx := contexthandler.WithAuthHTTPHeader(req.Context(), customHeader) + req = req.WithContext(ctx) + rp := NewReverseProxy(log.New("test"), func(req *http.Request) { req.Header.Set("X-KEY", "value") }) @@ -49,6 +55,7 @@ func TestReverseProxy(t *testing.T) { require.Empty(t, resp.Cookies()) require.Equal(t, "sandbox", resp.Header.Get("Content-Security-Policy")) require.NoError(t, resp.Body.Close()) + require.Empty(t, actualReq.Header.Get(customHeader)) }) t.Run("When proxying a request using WithModifyResponse should call it before default ModifyResponse func", func(t *testing.T) {
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-jv32-5578-pxjcghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-31130ghsaADVISORY
- github.com/grafana/grafana/commit/4dd56e4dabce10007bf4ba1059bf54178c35b177ghsaWEB
- github.com/grafana/grafana/commit/9da278c044ba605eb5a1886c48df9a2cb0d3885fghsaWEB
- github.com/grafana/grafana/releases/tag/v9.1.8ghsaWEB
- github.com/grafana/grafana/security/advisories/GHSA-jv32-5578-pxjcghsaWEB
News mentions
0No linked articles in our index yet.