Security: Make proxy endpoints not leak sensitive HTTP headers
Fixes CVE-2022-31130 (cherry picked from commit 2974574a53ab6d26be7b706e76271173a91fea3a)
This commit is contained in:
committed by
dsotirakis
parent
f68f621342
commit
4dd56e4dab
@@ -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)
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user