Middleware: Add CSP Report Only support (#58074)

* Middleware: Add CSP Report Only support

* Update docs/sources/setup-grafana/configure-grafana/_index.md

Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com>

* Update docs/sources/setup-grafana/configure-grafana/_index.md

Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com>

* Update csp documentation wording

* Update conf/sample.ini

Co-authored-by: Dave Henderson <dave.henderson@grafana.com>

* Update docs/sources/setup-grafana/configure-grafana/_index.md

Co-authored-by: Dave Henderson <dave.henderson@grafana.com>

* Update docs/sources/setup-grafana/configure-grafana/_index.md

Co-authored-by: Dave Henderson <dave.henderson@grafana.com>

* Update docs/sources/setup-grafana/configure-grafana/_index.md

Co-authored-by: Dave Henderson <dave.henderson@grafana.com>

* Update pkg/middleware/csp.go

Co-authored-by: Dave Henderson <dave.henderson@grafana.com>

Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com>
Co-authored-by: Dave Henderson <dave.henderson@grafana.com>
pull/58856/head
João Calisto 3 years ago committed by GitHub
parent aea860a3bd
commit f254a37d35
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      conf/defaults.ini
  2. 8
      conf/sample.ini
  3. 11
      docs/sources/setup-grafana/configure-grafana/_index.md
  4. 5
      pkg/api/http_server.go
  5. 92
      pkg/middleware/csp.go
  6. 43
      pkg/middleware/middleware_test.go
  7. 13
      pkg/middleware/testing.go
  8. 18
      pkg/setting/setting.go

@ -329,6 +329,15 @@ content_security_policy = false
# $ROOT_PATH is server.root_url without the protocol.
content_security_policy_template = """script-src 'self' 'unsafe-eval' 'unsafe-inline' 'strict-dynamic' $NONCE;object-src 'none';font-src 'self';style-src 'self' 'unsafe-inline' blob:;img-src * data:;base-uri 'self';connect-src 'self' grafana.com ws://$ROOT_PATH wss://$ROOT_PATH;manifest-src 'self';media-src 'none';form-action 'self';"""
# Enable adding the Content-Security-Policy-Report-Only header to your requests.
# Allows you to monitor the effects of a policy without enforcing it.
content_security_policy_report_only = false
# Set Content Security Policy Report Only template used when adding the Content-Security-Policy-Report-Only header to your requests.
# $NONCE in the template includes a random nonce.
# $ROOT_PATH is server.root_url without the protocol.
content_security_policy_report_only_template = """script-src 'self' 'unsafe-eval' 'unsafe-inline' 'strict-dynamic' $NONCE;object-src 'none';font-src 'self';style-src 'self' 'unsafe-inline' blob:;img-src * data:;base-uri 'self';connect-src 'self' grafana.com ws://$ROOT_PATH wss://$ROOT_PATH;manifest-src 'self';media-src 'none';form-action 'self';"""
# Controls if old angular plugins are supported or not. This will be disabled by default in future release
angular_support_enabled = true

@ -330,6 +330,14 @@
# $ROOT_PATH is server.root_url without the protocol.
;content_security_policy_template = """script-src 'self' 'unsafe-eval' 'unsafe-inline' 'strict-dynamic' $NONCE;object-src 'none';font-src 'self';style-src 'self' 'unsafe-inline' blob:;img-src * data:;base-uri 'self';connect-src 'self' grafana.com ws://$ROOT_PATH wss://$ROOT_PATH;manifest-src 'self';media-src 'none';form-action 'self';"""
# Enable adding the Content-Security-Policy-Report-Only header to your requests.
# Allows you to monitor the effects of a policy without enforcing it.
;content_security_policy_report_only = false
# Set Content Security Policy Report Only template used when adding the Content-Security-Policy-Report-Only header to your requests.
# $NONCE in the template includes a random nonce.
# $ROOT_PATH is server.root_url without the protocol.
;content_security_policy_report_only_template = """script-src 'self' 'unsafe-eval' 'unsafe-inline' 'strict-dynamic' $NONCE;object-src 'none';font-src 'self';style-src 'self' 'unsafe-inline' blob:;img-src * data:;base-uri 'self';connect-src 'self' grafana.com ws://$ROOT_PATH wss://$ROOT_PATH;manifest-src 'self';media-src 'none';form-action 'self';"""
# Controls if old angular plugins are supported or not. This will be disabled by default in future release
;angular_support_enabled = true

@ -623,7 +623,16 @@ Set to `true` to add the Content-Security-Policy header to your requests. CSP al
### content_security_policy_template
Set Content Security Policy template used when adding the Content-Security-Policy header to your requests. `$NONCE` in the template includes a random nonce.
Set the policy template that will be used when adding the `Content-Security-Policy` header to your requests. `$NONCE` in the template includes a random nonce.
### content_security_policy_report_only
Set to `true` to add the `Content-Security-Policy-Report-Only` header to your requests. CSP in Report Only mode enables you to experiment with policies by monitoring their effects without enforcing them.
You can enable both policies simultaneously.
### content_security_policy_template
Set the policy template that will be used when adding the `Content-Security-Policy-Report-Only` header to your requests. `$NONCE` in the template includes a random nonce.
<hr />

@ -623,7 +623,10 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
}
m.Use(middleware.HandleNoCacheHeader)
m.UseMiddleware(middleware.AddCSPHeader(hs.Cfg, hs.log))
if hs.Cfg.CSPEnabled || hs.Cfg.CSPReportOnlyEnabled {
m.UseMiddleware(middleware.ContentSecurityPolicy(hs.Cfg, hs.log))
}
for _, mw := range hs.middlewares {
m.Use(mw)

@ -14,40 +14,64 @@ import (
"github.com/grafana/grafana/pkg/setting"
)
// AddCSPHeader adds the Content Security Policy header.
func AddCSPHeader(cfg *setting.Cfg, logger log.Logger) func(http.Handler) http.Handler {
// ContentSecurityPolicy sets the configured Content-Security-Policy and/or Content-Security-Policy-Report-Only header(s) in the response.
func ContentSecurityPolicy(cfg *setting.Cfg, logger log.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if !cfg.CSPEnabled {
next.ServeHTTP(rw, req)
return
}
logger.Debug("Adding CSP header to response", "cfg", fmt.Sprintf("%p", cfg))
ctx := contexthandler.FromContext(req.Context())
if cfg.CSPTemplate == "" {
logger.Debug("CSP template not configured, so returning 500")
ctx.JsonApiErr(500, "CSP template has to be configured", nil)
return
}
var buf [16]byte
if _, err := io.ReadFull(rand.Reader, buf[:]); err != nil {
logger.Error("Failed to generate CSP nonce", "err", err)
ctx.JsonApiErr(500, "Failed to generate CSP nonce", err)
}
nonce := base64.RawStdEncoding.EncodeToString(buf[:])
val := strings.ReplaceAll(cfg.CSPTemplate, "$NONCE", fmt.Sprintf("'nonce-%s'", nonce))
re := regexp.MustCompile(`^\w+:(//)?`)
rootPath := re.ReplaceAllString(cfg.AppURL, "")
val = strings.ReplaceAll(val, "$ROOT_PATH", rootPath)
rw.Header().Set("Content-Security-Policy", val)
ctx.RequestNonce = nonce
logger.Debug("Successfully generated CSP nonce", "nonce", nonce)
next.ServeHTTP(rw, req)
})
if cfg.CSPEnabled {
next = cspMiddleware(cfg, next, logger)
}
if cfg.CSPReportOnlyEnabled {
next = cspReportOnlyMiddleware(cfg, next, logger)
}
next = nonceMiddleware(next, logger)
return next
}
}
func nonceMiddleware(next http.Handler, logger log.Logger) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
ctx := contexthandler.FromContext(req.Context())
nonce, err := generateNonce()
if err != nil {
logger.Error("Failed to generate CSP nonce", "err", err)
ctx.JsonApiErr(500, "Failed to generate CSP nonce", err)
}
ctx.RequestNonce = nonce
logger.Debug("Successfully generated CSP nonce", "nonce", nonce)
next.ServeHTTP(rw, req)
})
}
func cspMiddleware(cfg *setting.Cfg, next http.Handler, logger log.Logger) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
ctx := contexthandler.FromContext(req.Context())
policy := replacePolicyVariables(cfg.CSPTemplate, cfg.AppURL, ctx.RequestNonce)
rw.Header().Set("Content-Security-Policy", policy)
next.ServeHTTP(rw, req)
})
}
func cspReportOnlyMiddleware(cfg *setting.Cfg, next http.Handler, logger log.Logger) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
ctx := contexthandler.FromContext(req.Context())
policy := replacePolicyVariables(cfg.CSPReportOnlyTemplate, cfg.AppURL, ctx.RequestNonce)
rw.Header().Set("Content-Security-Policy-Report-Only", policy)
next.ServeHTTP(rw, req)
})
}
func replacePolicyVariables(policyTemplate, appURL, nonce string) string {
policy := strings.ReplaceAll(policyTemplate, "$NONCE", fmt.Sprintf("'nonce-%s'", nonce))
re := regexp.MustCompile(`^\w+:(//)?`)
rootPath := re.ReplaceAllString(appURL, "")
policy = strings.ReplaceAll(policy, "$ROOT_PATH", rootPath)
return policy
}
func generateNonce() (string, error) {
var buf [16]byte
if _, err := io.ReadFull(rand.Reader, buf[:]); err != nil {
return "", err
}
return base64.RawStdEncoding.EncodeToString(buf[:]), nil
}

@ -83,6 +83,47 @@ func TestMiddleWareSecurityHeaders(t *testing.T) {
})
}
func TestMiddleWareContentSecurityPolicyHeaders(t *testing.T) {
policy := `script-src 'self' 'strict-dynamic' 'nonce-[^']+';connect-src 'self' ws://localhost:3000/ wss://localhost:3000/;`
middlewareScenario(t, "middleware should add Content-Security-Policy", func(t *testing.T, sc *scenarioContext) {
sc.fakeReq("GET", "/api/").exec()
assert.Regexp(t, policy, sc.resp.Header().Get("Content-Security-Policy"))
}, func(cfg *setting.Cfg) {
cfg.CSPEnabled = true
cfg.CSPTemplate = "script-src 'self' 'strict-dynamic' $NONCE;connect-src 'self' ws://$ROOT_PATH wss://$ROOT_PATH;"
cfg.AppURL = "http://localhost:3000/"
})
middlewareScenario(t, "middleware should add Content-Security-Policy-Report-Only", func(t *testing.T, sc *scenarioContext) {
sc.fakeReq("GET", "/api/").exec()
assert.Regexp(t, policy, sc.resp.Header().Get("Content-Security-Policy-Report-Only"))
}, func(cfg *setting.Cfg) {
cfg.CSPReportOnlyEnabled = true
cfg.CSPReportOnlyTemplate = "script-src 'self' 'strict-dynamic' $NONCE;connect-src 'self' ws://$ROOT_PATH wss://$ROOT_PATH;"
cfg.AppURL = "http://localhost:3000/"
})
middlewareScenario(t, "middleware can add both CSP and CSP-Report-Only", func(t *testing.T, sc *scenarioContext) {
sc.fakeReq("GET", "/api/").exec()
cspHeader := sc.resp.Header().Get("Content-Security-Policy")
cspReportOnlyHeader := sc.resp.Header().Get("Content-Security-Policy-Report-Only")
assert.Regexp(t, policy, cspHeader)
assert.Regexp(t, policy, cspReportOnlyHeader)
// assert CSP-Report-Only reuses the same nonce as CSP
assert.Equal(t, cspHeader, cspReportOnlyHeader)
}, func(cfg *setting.Cfg) {
cfg.CSPEnabled = true
cfg.CSPTemplate = "script-src 'self' 'strict-dynamic' $NONCE;connect-src 'self' ws://$ROOT_PATH wss://$ROOT_PATH;"
cfg.CSPReportOnlyEnabled = true
cfg.CSPReportOnlyTemplate = "script-src 'self' 'strict-dynamic' $NONCE;connect-src 'self' ws://$ROOT_PATH wss://$ROOT_PATH;"
cfg.AppURL = "http://localhost:3000/"
})
}
func TestMiddlewareContext(t *testing.T) {
const noCache = "no-cache"
@ -770,7 +811,7 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc, cbs ...func(
sc.m = web.New()
sc.m.Use(AddDefaultResponseHeaders(cfg))
sc.m.UseMiddleware(AddCSPHeader(cfg, logger))
sc.m.UseMiddleware(ContentSecurityPolicy(cfg, logger))
sc.m.UseMiddleware(web.Renderer(viewsPath, "[[", "]]"))
sc.mockSQLStore = dbtest.NewFakeDB()

@ -15,6 +15,7 @@ import (
"github.com/grafana/grafana/pkg/services/apikey/apikeytest"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/contexthandler"
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
"github.com/grafana/grafana/pkg/services/login/loginservice"
"github.com/grafana/grafana/pkg/services/org/orgtest"
"github.com/grafana/grafana/pkg/services/user/usertest"
@ -77,7 +78,11 @@ func (sc *scenarioContext) fakeReq(method, url string) *scenarioContext {
sc.resp = httptest.NewRecorder()
req, err := http.NewRequest(method, url, nil)
require.NoError(sc.t, err)
sc.req = req
reqCtx := &models.ReqContext{
Context: web.FromContext(req.Context()),
}
sc.req = req.WithContext(ctxkey.Set(req.Context(), reqCtx))
return sc
}
@ -95,7 +100,11 @@ func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map
}
req.URL.RawQuery = q.Encode()
require.NoError(sc.t, err)
sc.req = req
reqCtx := &models.ReqContext{
Context: web.FromContext(req.Context()),
}
sc.req = req.WithContext(ctxkey.Set(req.Context(), reqCtx))
return sc
}

@ -263,7 +263,11 @@ type Cfg struct {
// CSPEnabled toggles Content Security Policy support.
CSPEnabled bool
// CSPTemplate contains the Content Security Policy template.
CSPTemplate string
CSPTemplate string
// CSPReportEnabled toggles Content Security Policy Report Only support.
CSPReportOnlyEnabled bool
// CSPReportOnlyTemplate contains the Content Security Policy Report Only template.
CSPReportOnlyTemplate string
AngularSupportEnabled bool
TempDataLifetime time.Duration
@ -1285,9 +1289,19 @@ func readSecuritySettings(iniFile *ini.File, cfg *Cfg) error {
cfg.StrictTransportSecurityMaxAge = security.Key("strict_transport_security_max_age_seconds").MustInt(86400)
cfg.StrictTransportSecurityPreload = security.Key("strict_transport_security_preload").MustBool(false)
cfg.StrictTransportSecuritySubDomains = security.Key("strict_transport_security_subdomains").MustBool(false)
cfg.AngularSupportEnabled = security.Key("angular_support_enabled").MustBool(true)
cfg.CSPEnabled = security.Key("content_security_policy").MustBool(false)
cfg.CSPTemplate = security.Key("content_security_policy_template").MustString("")
cfg.AngularSupportEnabled = security.Key("angular_support_enabled").MustBool(true)
cfg.CSPReportOnlyEnabled = security.Key("content_security_policy_report_only").MustBool(false)
cfg.CSPReportOnlyTemplate = security.Key("content_security_policy_report_only_template").MustString("")
if cfg.CSPEnabled && cfg.CSPTemplate == "" {
return fmt.Errorf("enabling content_security_policy requires a content_security_policy_template configuration")
}
if cfg.CSPReportOnlyEnabled && cfg.CSPReportOnlyTemplate == "" {
return fmt.Errorf("enabling content_security_policy_report_only requires a content_security_policy_report_only_template configuration")
}
// read data source proxy whitelist
DataProxyWhiteList = make(map[string]bool)

Loading…
Cancel
Save