Repository: labstack/echo (go)

Issue:
Add support for Content-Security-Policy-Report-Only header
### Issue Description
While developing websites, it's a common practice to set the CSP to report-only mode before the set of hosts are finalized. Would it be possible to add support to switch on the report-only mode via a middleware flag or something? Right now, I have to go into `middleware.SecureWithConfig` code in `secure.go` file, and manually modify this line:

```
res.Header().Set(echo.HeaderContentSecurityPolicy, config.ContentSecurityPolicy)
```
to
```
res.Header().Set(echo.HeaderContentSecurityPolicy+"-Report-Only", config.ContentSecurityPolicy)
```

### Checklist

- [x] Dependencies installed
- [x] No typos
- [x] Searched existing issues and docs


Pull request:
feat(secure): support Content-Security-Policy-Report-Only header
Closes #1283

Patch:
diff --git a/echo.go b/echo.go
index 4f8209b..409dfad 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 8839c87..eded54f 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)
 		}


Test patch:
diff --git a/middleware/secure_test.go b/middleware/secure_test.go
index d1cbf18..47121fb 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))
 }