Skip to content

Commit 9da278c

Browse files
marefrpapagian
authored andcommitted
Plugins: Make proxy endpoints not leak sensitive HTTP headers
Fixes CVE-2022-31130 (cherry picked from commit 40b319d3d6a9945c05709ce8d4679407f6ccadf0)
1 parent c4e6e05 commit 9da278c

File tree

9 files changed

+102
-2
lines changed

9 files changed

+102
-2
lines changed

pkg/api/plugin_resource.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515

1616
"github.com/grafana/grafana/pkg/models"
1717
"github.com/grafana/grafana/pkg/plugins/backendplugin"
18+
"github.com/grafana/grafana/pkg/services/contexthandler"
1819
"github.com/grafana/grafana/pkg/services/datasources"
1920
"github.com/grafana/grafana/pkg/util/proxyutil"
2021
"github.com/grafana/grafana/pkg/web"
@@ -118,6 +119,14 @@ func (hs *HTTPServer) makePluginResourceRequest(w http.ResponseWriter, req *http
118119
hs.log.Warn("failed to unpack JSONData in datasource instance settings", "err", err)
119120
}
120121
}
122+
123+
list := contexthandler.AuthHTTPHeaderListFromContext(req.Context())
124+
if list != nil {
125+
for _, name := range list.Items {
126+
req.Header.Del(name)
127+
}
128+
}
129+
121130
proxyutil.ClearCookieHeader(req, keepCookieModel.KeepCookies)
122131
proxyutil.PrepareProxyRequest(req)
123132

pkg/api/plugins_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/grafana/grafana/pkg/infra/log/logtest"
2222
"github.com/grafana/grafana/pkg/models"
2323
"github.com/grafana/grafana/pkg/plugins"
24+
"github.com/grafana/grafana/pkg/services/contexthandler"
2425
"github.com/grafana/grafana/pkg/services/quota/quotatest"
2526
"github.com/grafana/grafana/pkg/setting"
2627
"github.com/grafana/grafana/pkg/web/webtest"
@@ -271,6 +272,12 @@ func TestMakePluginResourceRequest(t *testing.T) {
271272
pluginClient: pluginClient,
272273
}
273274
req := httptest.NewRequest(http.MethodGet, "/", nil)
275+
276+
const customHeader = "X-CUSTOM"
277+
req.Header.Set(customHeader, "val")
278+
ctx := contexthandler.WithAuthHTTPHeader(req.Context(), customHeader)
279+
req = req.WithContext(ctx)
280+
274281
resp := httptest.NewRecorder()
275282
pCtx := backend.PluginContext{}
276283
err := hs.makePluginResourceRequest(resp, req, pCtx)
@@ -283,6 +290,7 @@ func TestMakePluginResourceRequest(t *testing.T) {
283290
}
284291

285292
require.Equal(t, "sandbox", resp.Header().Get("Content-Security-Policy"))
293+
require.Empty(t, req.Header.Get(customHeader))
286294
}
287295

288296
func callGetPluginAsset(sc *scenarioContext) {

pkg/middleware/middleware_basic_auth_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ func TestMiddlewareBasicAuth(t *testing.T) {
3737
assert.True(t, sc.context.IsSignedIn)
3838
assert.Equal(t, orgID, sc.context.OrgId)
3939
assert.Equal(t, models.ROLE_EDITOR, sc.context.OrgRole)
40+
list := contexthandler.AuthHTTPHeaderListFromContext(sc.context.Req.Context())
41+
require.NotNil(t, list)
42+
require.EqualValues(t, []string{"Authorization"}, list.Items)
4043
}, configure)
4144

4245
middlewareScenario(t, "Handle auth", func(t *testing.T, sc *scenarioContext) {
@@ -70,6 +73,9 @@ func TestMiddlewareBasicAuth(t *testing.T) {
7073

7174
assert.True(t, sc.context.IsSignedIn)
7275
assert.Equal(t, id, sc.context.UserId)
76+
list := contexthandler.AuthHTTPHeaderListFromContext(sc.context.Req.Context())
77+
require.NotNil(t, list)
78+
require.EqualValues(t, []string{"Authorization"}, list.Items)
7379
}, configure)
7480

7581
middlewareScenario(t, "Should return error if user is not found", func(t *testing.T, sc *scenarioContext) {

pkg/middleware/middleware_jwt_auth_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"testing"
77

88
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
910

1011
"github.com/grafana/grafana/pkg/models"
1112
"github.com/grafana/grafana/pkg/services/contexthandler"
@@ -55,6 +56,9 @@ func TestMiddlewareJWTAuth(t *testing.T) {
5556
assert.Equal(t, orgID, sc.context.OrgId)
5657
assert.Equal(t, id, sc.context.UserId)
5758
assert.Equal(t, myUsername, sc.context.Login)
59+
list := contexthandler.AuthHTTPHeaderListFromContext(sc.context.Req.Context())
60+
require.NotNil(t, list)
61+
require.EqualValues(t, []string{sc.cfg.JWTAuthHeaderName}, list.Items)
5862
}, configure, configureUsernameClaim)
5963

6064
middlewareScenario(t, "Valid token with valid email claim", func(t *testing.T, sc *scenarioContext) {

pkg/middleware/middleware_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,11 @@ func TestMiddlewareContext(t *testing.T) {
396396
assert.True(t, sc.context.IsSignedIn)
397397
assert.Equal(t, userID, sc.context.UserId)
398398
assert.Equal(t, orgID, sc.context.OrgId)
399+
list := contexthandler.AuthHTTPHeaderListFromContext(sc.context.Req.Context())
400+
require.NotNil(t, list)
401+
require.Contains(t, list.Items, sc.cfg.AuthProxyHeaderName)
402+
require.Contains(t, list.Items, "X-WEBAUTH-GROUPS")
403+
require.Contains(t, list.Items, "X-WEBAUTH-ROLE")
399404
}, func(cfg *setting.Cfg) {
400405
configure(cfg)
401406
cfg.LDAPEnabled = false

pkg/services/contexthandler/auth_jwt.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ func (h *ContextHandler) initContextWithJWT(ctx *models.ReqContext, orgId int64)
9999
return true
100100
}
101101

102+
newCtx := WithAuthHTTPHeader(ctx.Req.Context(), h.Cfg.JWTAuthHeaderName)
103+
*ctx.Req = *ctx.Req.WithContext(newCtx)
104+
102105
ctx.SignedInUser = query.Result
103106
ctx.IsSignedIn = true
104107

pkg/services/contexthandler/contexthandler.go

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,9 @@ func (h *ContextHandler) initContextWithAPIKey(reqContext *models.ReqContext) bo
244244
_, span := h.tracer.Start(reqContext.Req.Context(), "initContextWithAPIKey")
245245
defer span.End()
246246

247+
ctx := WithAuthHTTPHeader(reqContext.Req.Context(), "Authorization")
248+
*reqContext.Req = *reqContext.Req.WithContext(ctx)
249+
247250
var (
248251
apikey *models.ApiKey
249252
errKey error
@@ -326,7 +329,7 @@ func (h *ContextHandler) initContextWithBasicAuth(reqContext *models.ReqContext,
326329
return false
327330
}
328331

329-
ctx, span := h.tracer.Start(reqContext.Req.Context(), "initContextWithBasicAuth")
332+
_, span := h.tracer.Start(reqContext.Req.Context(), "initContextWithBasicAuth")
330333
defer span.End()
331334

332335
username, password, err := util.DecodeBasicAuthHeader(header)
@@ -335,12 +338,15 @@ func (h *ContextHandler) initContextWithBasicAuth(reqContext *models.ReqContext,
335338
return true
336339
}
337340

341+
ctx := WithAuthHTTPHeader(reqContext.Req.Context(), "Authorization")
342+
*reqContext.Req = *reqContext.Req.WithContext(ctx)
343+
338344
authQuery := models.LoginUserQuery{
339345
Username: username,
340346
Password: password,
341347
Cfg: h.Cfg,
342348
}
343-
if err := h.authenticator.AuthenticateUser(reqContext.Req.Context(), &authQuery); err != nil {
349+
if err := h.authenticator.AuthenticateUser(ctx, &authQuery); err != nil {
344350
reqContext.Logger.Debug(
345351
"Failed to authorize the user",
346352
"username", username,
@@ -571,6 +577,15 @@ func (h *ContextHandler) initContextWithAuthProxy(reqContext *models.ReqContext,
571577

572578
logger.Debug("Successfully got user info", "userID", user.UserId, "username", user.Login)
573579

580+
ctx := WithAuthHTTPHeader(reqContext.Req.Context(), h.Cfg.AuthProxyHeaderName)
581+
for _, header := range h.Cfg.AuthProxyHeaders {
582+
if header != "" {
583+
ctx = WithAuthHTTPHeader(ctx, header)
584+
}
585+
}
586+
587+
*reqContext.Req = *reqContext.Req.WithContext(ctx)
588+
574589
// Add user info to context
575590
reqContext.SignedInUser = user
576591
reqContext.IsSignedIn = true
@@ -590,3 +605,38 @@ func (h *ContextHandler) initContextWithAuthProxy(reqContext *models.ReqContext,
590605

591606
return true
592607
}
608+
609+
type authHTTPHeaderListContextKey struct{}
610+
611+
var authHTTPHeaderListKey = authHTTPHeaderListContextKey{}
612+
613+
// AuthHTTPHeaderList used to record HTTP headers that being when verifying authentication
614+
// of an incoming HTTP request.
615+
type AuthHTTPHeaderList struct {
616+
Items []string
617+
}
618+
619+
// WithAuthHTTPHeader returns a copy of parent in which the named HTTP header will be included
620+
// and later retrievable by AuthHTTPHeaderListFromContext.
621+
func WithAuthHTTPHeader(parent context.Context, name string) context.Context {
622+
list := AuthHTTPHeaderListFromContext(parent)
623+
624+
if list == nil {
625+
list = &AuthHTTPHeaderList{
626+
Items: []string{},
627+
}
628+
}
629+
630+
list.Items = append(list.Items, name)
631+
632+
return context.WithValue(parent, authHTTPHeaderListKey, list)
633+
}
634+
635+
// AuthHTTPHeaderListFromContext returns the AuthHTTPHeaderList in a context.Context, if any,
636+
// and will include any HTTP headers used when verifying authentication of an incoming HTTP request.
637+
func AuthHTTPHeaderListFromContext(c context.Context) *AuthHTTPHeaderList {
638+
if list, ok := c.Value(authHTTPHeaderListKey).(*AuthHTTPHeaderList); ok {
639+
return list
640+
}
641+
return nil
642+
}

pkg/util/proxyutil/reverse_proxy.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"time"
1111

1212
glog "github.com/grafana/grafana/pkg/infra/log"
13+
"github.com/grafana/grafana/pkg/services/contexthandler"
1314
)
1415

1516
// StatusClientClosedRequest A non-standard status code introduced by nginx
@@ -66,6 +67,13 @@ func NewReverseProxy(logger glog.Logger, director func(*http.Request), opts ...R
6667
// wrapDirector wraps a director and adds additional functionality.
6768
func wrapDirector(d func(*http.Request)) func(req *http.Request) {
6869
return func(req *http.Request) {
70+
list := contexthandler.AuthHTTPHeaderListFromContext(req.Context())
71+
if list != nil {
72+
for _, name := range list.Items {
73+
req.Header.Del(name)
74+
}
75+
}
76+
6977
d(req)
7078
PrepareProxyRequest(req)
7179

pkg/util/proxyutil/reverse_proxy_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"time"
1010

1111
"github.com/grafana/grafana/pkg/infra/log"
12+
"github.com/grafana/grafana/pkg/services/contexthandler"
1213
"github.com/stretchr/testify/require"
1314
)
1415

@@ -30,6 +31,11 @@ func TestReverseProxy(t *testing.T) {
3031
req.Header.Set("Referer", "https://test.com/api")
3132
req.RemoteAddr = "10.0.0.1"
3233

34+
const customHeader = "X-CUSTOM"
35+
req.Header.Set(customHeader, "val")
36+
ctx := contexthandler.WithAuthHTTPHeader(req.Context(), customHeader)
37+
req = req.WithContext(ctx)
38+
3339
rp := NewReverseProxy(log.New("test"), func(req *http.Request) {
3440
req.Header.Set("X-KEY", "value")
3541
})
@@ -49,6 +55,7 @@ func TestReverseProxy(t *testing.T) {
4955
require.Empty(t, resp.Cookies())
5056
require.Equal(t, "sandbox", resp.Header.Get("Content-Security-Policy"))
5157
require.NoError(t, resp.Body.Close())
58+
require.Empty(t, actualReq.Header.Get(customHeader))
5259
})
5360

5461
t.Run("When proxying a request using WithModifyResponse should call it before default ModifyResponse func", func(t *testing.T) {

0 commit comments

Comments
 (0)