Middleware: Add CSP support (#29740)

* Middleware: Add support for CSP

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

Co-authored by @iOrcohen
This commit is contained in:
Arve Knudsen
2021-01-12 07:42:32 +01:00
committed by GitHub
parent 4ed901e1f9
commit 50b649a869
19 changed files with 449 additions and 222 deletions

View File

@@ -25,6 +25,8 @@ type IndexViewData struct {
AppleTouchIcon template.URL
AppTitle string
Sentry *setting.Sentry
// Nonce is a cryptographic identifier for use with Content Security Policy.
Nonce string
}
const (

View File

@@ -346,7 +346,8 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
m.Use(middleware.ValidateHostHeader(hs.Cfg))
}
m.Use(middleware.HandleNoCacheHeader())
m.Use(middleware.HandleNoCacheHeader)
m.Use(middleware.AddCSPHeader(hs.Cfg, hs.log))
for _, mw := range hs.middlewares {
m.Use(mw)

View File

@@ -427,6 +427,7 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat
AppTitle: "Grafana",
NavTree: navTree,
Sentry: &hs.Cfg.Sentry,
Nonce: c.RequestNonce,
}
if setting.DisableGravatar {

View File

@@ -21,14 +21,14 @@ import (
)
const (
ViewIndex = "index"
LoginErrorCookieName = "login_error"
viewIndex = "index"
loginErrorCookieName = "login_error"
)
var setIndexViewData = (*HTTPServer).setIndexViewData
var getViewIndex = func() string {
return ViewIndex
return viewIndex
}
func (hs *HTTPServer) ValidateRedirectTo(redirectTo string) error {
@@ -96,12 +96,12 @@ func (hs *HTTPServer) LoginView(c *models.ReqContext) {
viewData.Settings["oauth"] = enabledOAuths
viewData.Settings["samlEnabled"] = hs.License.HasValidLicense() && hs.Cfg.SAMLEnabled
if loginError, ok := tryGetEncryptedCookie(c, LoginErrorCookieName); ok {
if loginError, ok := tryGetEncryptedCookie(c, loginErrorCookieName); ok {
// this cookie is only set whenever an OAuth login fails
// therefore the loginError should be passed to the view data
// and the view should return immediately before attempting
// to login again via OAuth and enter to a redirect loop
cookies.DeleteCookie(c.Resp, LoginErrorCookieName, hs.CookieOptionsFromCfg)
cookies.DeleteCookie(c.Resp, loginErrorCookieName, hs.CookieOptionsFromCfg)
viewData.Settings["loginError"] = loginError
c.HTML(200, getViewIndex(), viewData)
return
@@ -317,7 +317,7 @@ func (hs *HTTPServer) trySetEncryptedCookie(ctx *models.ReqContext, cookieName s
func (hs *HTTPServer) redirectWithError(ctx *models.ReqContext, err error, v ...interface{}) {
ctx.Logger.Error(err.Error(), v...)
if err := hs.trySetEncryptedCookie(ctx, LoginErrorCookieName, err.Error(), 60); err != nil {
if err := hs.trySetEncryptedCookie(ctx, loginErrorCookieName, err.Error(), 60); err != nil {
hs.log.Error("Failed to set encrypted cookie", "err", err)
}
@@ -326,7 +326,7 @@ func (hs *HTTPServer) redirectWithError(ctx *models.ReqContext, err error, v ...
func (hs *HTTPServer) RedirectResponseWithError(ctx *models.ReqContext, err error, v ...interface{}) *RedirectResponse {
ctx.Logger.Error(err.Error(), v...)
if err := hs.trySetEncryptedCookie(ctx, LoginErrorCookieName, err.Error(), 60); err != nil {
if err := hs.trySetEncryptedCookie(ctx, loginErrorCookieName, err.Error(), 60); err != nil {
hs.log.Error("Failed to set encrypted cookie", "err", err)
}

View File

@@ -103,25 +103,34 @@ func TestLoginErrorCookieAPIEndpoint(t *testing.T) {
cfg.LoginCookieName = "grafana_session"
setting.SecretKey = "login_testing"
setting.OAuthService = &setting.OAuther{}
setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo)
setting.OAuthService.OAuthInfos["github"] = &setting.OAuthInfo{
ClientId: "fake",
ClientSecret: "fakefake",
Enabled: true,
AllowSignup: true,
Name: "github",
origOAuthService := setting.OAuthService
origOAuthAutoLogin := setting.OAuthAutoLogin
t.Cleanup(func() {
setting.OAuthService = origOAuthService
setting.OAuthAutoLogin = origOAuthAutoLogin
})
setting.OAuthService = &setting.OAuther{
OAuthInfos: map[string]*setting.OAuthInfo{
"github": {
ClientId: "fake",
ClientSecret: "fakefake",
Enabled: true,
AllowSignup: true,
Name: "github",
},
},
}
setting.OAuthAutoLogin = true
oauthError := errors.New("User not a member of one of the required organizations")
encryptedError, _ := util.Encrypt([]byte(oauthError.Error()), setting.SecretKey)
encryptedError, err := util.Encrypt([]byte(oauthError.Error()), setting.SecretKey)
require.NoError(t, err)
expCookiePath := "/"
if len(setting.AppSubUrl) > 0 {
expCookiePath = setting.AppSubUrl
}
cookie := http.Cookie{
Name: LoginErrorCookieName,
Name: loginErrorCookieName,
MaxAge: 60,
Value: hex.EncodeToString(encryptedError),
HttpOnly: true,
@@ -131,10 +140,10 @@ func TestLoginErrorCookieAPIEndpoint(t *testing.T) {
}
sc.m.Get(sc.url, sc.defaultHandler)
sc.fakeReqNoAssertionsWithCookie("GET", sc.url, cookie).exec()
assert.Equal(t, sc.resp.Code, 200)
require.Equal(t, 200, sc.resp.Code)
responseString, err := getBody(sc.resp)
assert.NoError(t, err)
require.NoError(t, err)
assert.True(t, strings.Contains(responseString, oauthError.Error()))
}
@@ -276,7 +285,7 @@ func TestLoginViewRedirect(t *testing.T) {
}
sc.m.Get(sc.url, sc.defaultHandler)
sc.fakeReqNoAssertionsWithCookie("GET", sc.url, cookie).exec()
assert.Equal(t, c.status, sc.resp.Code)
require.Equal(t, c.status, sc.resp.Code)
if c.status == 302 {
location, ok := sc.resp.Header()["Location"]
assert.True(t, ok)
@@ -304,7 +313,7 @@ func TestLoginViewRedirect(t *testing.T) {
}
responseString, err := getBody(sc.resp)
assert.NoError(t, err)
require.NoError(t, err)
if c.err != nil {
assert.True(t, strings.Contains(responseString, c.err.Error()))
}
@@ -443,10 +452,10 @@ func TestLoginPostRedirect(t *testing.T) {
}
sc.m.Post(sc.url, sc.defaultHandler)
sc.fakeReqNoAssertionsWithCookie("POST", sc.url, cookie).exec()
assert.Equal(t, sc.resp.Code, 200)
require.Equal(t, 200, sc.resp.Code)
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
assert.NoError(t, err)
require.NoError(t, err)
redirectURL := respJSON.Get("redirectUrl").MustString()
if c.err != nil {
assert.Equal(t, "", redirectURL)
@@ -496,10 +505,10 @@ func TestLoginOAuthRedirect(t *testing.T) {
sc.m.Get(sc.url, sc.defaultHandler)
sc.fakeReqNoAssertions("GET", sc.url).exec()
assert.Equal(t, sc.resp.Code, 307)
require.Equal(t, 307, sc.resp.Code)
location, ok := sc.resp.Header()["Location"]
assert.True(t, ok)
assert.Equal(t, location[0], "/login/github")
assert.Equal(t, "/login/github", location[0])
}
func TestLoginInternal(t *testing.T) {
@@ -532,16 +541,16 @@ func TestLoginInternal(t *testing.T) {
sc.fakeReqNoAssertions("GET", sc.url).exec()
// Shouldn't redirect to the OAuth login URL
assert.Equal(t, sc.resp.Code, 200)
assert.Equal(t, 200, sc.resp.Code)
}
func TestAuthProxyLoginEnableLoginTokenDisabled(t *testing.T) {
sc := setupAuthProxyLoginTest(t, false)
assert.Equal(t, sc.resp.Code, 302)
require.Equal(t, 302, sc.resp.Code)
location, ok := sc.resp.Header()["Location"]
assert.True(t, ok)
assert.Equal(t, location[0], "/")
assert.Equal(t, "/", location[0])
_, ok = sc.resp.Header()["Set-Cookie"]
assert.False(t, ok, "Set-Cookie does not exist")
@@ -549,11 +558,11 @@ func TestAuthProxyLoginEnableLoginTokenDisabled(t *testing.T) {
func TestAuthProxyLoginWithEnableLoginToken(t *testing.T) {
sc := setupAuthProxyLoginTest(t, true)
require.Equal(t, sc.resp.Code, 302)
require.Equal(t, 302, sc.resp.Code)
location, ok := sc.resp.Header()["Location"]
assert.True(t, ok)
assert.Equal(t, location[0], "/")
assert.Equal(t, "/", location[0])
setCookie := sc.resp.Header()["Set-Cookie"]
require.NotNil(t, setCookie, "Set-Cookie should exist")
assert.Equal(t, "grafana_session=; Path=/; Max-Age=0; HttpOnly", setCookie[0])