mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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>
This commit is contained in:
parent
aea860a3bd
commit
f254a37d35
@ -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…
Reference in New Issue
Block a user