diff --git a/echo.go b/echo.go index 4f8209b73..409dfade2 100644 --- a/echo.go +++ b/echo.go @@ -212,12 +212,13 @@ const ( HeaderAccessControlMaxAge = "Access-Control-Max-Age" // Security - HeaderStrictTransportSecurity = "Strict-Transport-Security" - HeaderXContentTypeOptions = "X-Content-Type-Options" - HeaderXXSSProtection = "X-XSS-Protection" - HeaderXFrameOptions = "X-Frame-Options" - HeaderContentSecurityPolicy = "Content-Security-Policy" - HeaderXCSRFToken = "X-CSRF-Token" + HeaderStrictTransportSecurity = "Strict-Transport-Security" + HeaderXContentTypeOptions = "X-Content-Type-Options" + HeaderXXSSProtection = "X-XSS-Protection" + HeaderXFrameOptions = "X-Frame-Options" + HeaderContentSecurityPolicy = "Content-Security-Policy" + HeaderContentSecurityPolicyReportOnly = "Content-Security-Policy-Report-Only" + HeaderXCSRFToken = "X-CSRF-Token" ) const ( diff --git a/middleware/secure.go b/middleware/secure.go index 8839c879c..eded54f15 100644 --- a/middleware/secure.go +++ b/middleware/secure.go @@ -53,6 +53,13 @@ type ( // trusted web page context. // Optional. Default value "". ContentSecurityPolicy string `yaml:"content_security_policy"` + + // CSPReportOnly would use the `Content-Security-Policy-Report-Only` header instead + // of the `Content-Security-Policy` header. This allows iterative updates of the + // content security policy by only reporting the violations that would + // have occurred instead of blocking the resource. + // Optional. Default value false. + CSPReportOnly bool `yaml:"csp_report_only"` } ) @@ -108,7 +115,11 @@ func SecureWithConfig(config SecureConfig) echo.MiddlewareFunc { res.Header().Set(echo.HeaderStrictTransportSecurity, fmt.Sprintf("max-age=%d%s", config.HSTSMaxAge, subdomains)) } if config.ContentSecurityPolicy != "" { - res.Header().Set(echo.HeaderContentSecurityPolicy, config.ContentSecurityPolicy) + if config.CSPReportOnly { + res.Header().Set(echo.HeaderContentSecurityPolicyReportOnly, config.ContentSecurityPolicy) + } else { + res.Header().Set(echo.HeaderContentSecurityPolicy, config.ContentSecurityPolicy) + } } return next(c) } diff --git a/middleware/secure_test.go b/middleware/secure_test.go index d1cbf187a..47121fb32 100644 --- a/middleware/secure_test.go +++ b/middleware/secure_test.go @@ -42,4 +42,24 @@ func TestSecure(t *testing.T) { assert.Equal(t, "", rec.Header().Get(echo.HeaderXFrameOptions)) assert.Equal(t, "max-age=3600; includeSubdomains", rec.Header().Get(echo.HeaderStrictTransportSecurity)) assert.Equal(t, "default-src 'self'", rec.Header().Get(echo.HeaderContentSecurityPolicy)) + assert.Equal(t, "", rec.Header().Get(echo.HeaderContentSecurityPolicyReportOnly)) + + // Custom with CSPReportOnly flag + req.Header.Set(echo.HeaderXForwardedProto, "https") + rec = httptest.NewRecorder() + c = e.NewContext(req, rec) + SecureWithConfig(SecureConfig{ + XSSProtection: "", + ContentTypeNosniff: "", + XFrameOptions: "", + HSTSMaxAge: 3600, + ContentSecurityPolicy: "default-src 'self'", + CSPReportOnly: true, + })(h)(c) + assert.Equal(t, "", rec.Header().Get(echo.HeaderXXSSProtection)) + assert.Equal(t, "", rec.Header().Get(echo.HeaderXContentTypeOptions)) + assert.Equal(t, "", rec.Header().Get(echo.HeaderXFrameOptions)) + assert.Equal(t, "max-age=3600; includeSubdomains", rec.Header().Get(echo.HeaderStrictTransportSecurity)) + assert.Equal(t, "default-src 'self'", rec.Header().Get(echo.HeaderContentSecurityPolicyReportOnly)) + assert.Equal(t, "", rec.Header().Get(echo.HeaderContentSecurityPolicy)) }