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:
co-authored by
Christopher Moyer
Dave Henderson
parent
aea860a3bd
commit
f254a37d35
+58
-34
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user