From b37aaea36d282badab6d34bbd61da79bfc54d88d Mon Sep 17 00:00:00 2001 From: Roman Donchenko Date: Fri, 7 Jun 2024 10:24:04 +0300 Subject: [PATCH] Headers middleware: support Content-Security-Policy-Report-Only --- docs/content/middlewares/http/headers.md | 4 ++ .../dynamic-configuration/docker-labels.yml | 1 + .../reference/dynamic-configuration/file.toml | 1 + .../reference/dynamic-configuration/file.yaml | 1 + .../kubernetes-crd-definition-v1.yml | 4 ++ .../reference/dynamic-configuration/kv-ref.md | 1 + .../traefik.io_middlewares.yaml | 4 ++ integration/fixtures/k8s/01-traefik-crd.yml | 4 ++ pkg/config/dynamic/fixtures/sample.toml | 1 + pkg/config/dynamic/middlewares.go | 3 + pkg/config/label/label_test.go | 68 ++++++++++--------- pkg/middlewares/headers/secure.go | 37 +++++----- pkg/provider/kv/kv_test.go | 34 +++++----- pkg/redactor/redactor_config_test.go | 1 + .../testdata/anonymized-dynamic-config.json | 1 + .../testdata/secured-dynamic-config.json | 1 + .../components/_commons/PanelMiddlewares.vue | 16 +++++ 17 files changed, 116 insertions(+), 66 deletions(-) diff --git a/docs/content/middlewares/http/headers.md b/docs/content/middlewares/http/headers.md index d0cb63672..afeb6891f 100644 --- a/docs/content/middlewares/http/headers.md +++ b/docs/content/middlewares/http/headers.md @@ -394,6 +394,10 @@ This overrides the `BrowserXssFilter` option. The `contentSecurityPolicy` option allows the `Content-Security-Policy` header value to be set with a custom value. +### `contentSecurityPolicyReportOnly` + +The `contentSecurityPolicyReportOnly` option allows the `Content-Security-Policy-Report-Only` header value to be set with a custom value. + ### `publicKey` The `publicKey` implements HPKP to prevent MITM attacks with forged certificates. diff --git a/docs/content/reference/dynamic-configuration/docker-labels.yml b/docs/content/reference/dynamic-configuration/docker-labels.yml index 319988894..a3e65baa2 100644 --- a/docs/content/reference/dynamic-configuration/docker-labels.yml +++ b/docs/content/reference/dynamic-configuration/docker-labels.yml @@ -55,6 +55,7 @@ - "traefik.http.middlewares.middleware12.headers.allowedhosts=foobar, foobar" - "traefik.http.middlewares.middleware12.headers.browserxssfilter=true" - "traefik.http.middlewares.middleware12.headers.contentsecuritypolicy=foobar" +- "traefik.http.middlewares.middleware12.headers.contentsecuritypolicyreportonly=foobar" - "traefik.http.middlewares.middleware12.headers.contenttypenosniff=true" - "traefik.http.middlewares.middleware12.headers.custombrowserxssvalue=foobar" - "traefik.http.middlewares.middleware12.headers.customframeoptionsvalue=foobar" diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml index a42f9ba17..cd0ffde60 100644 --- a/docs/content/reference/dynamic-configuration/file.toml +++ b/docs/content/reference/dynamic-configuration/file.toml @@ -198,6 +198,7 @@ browserXssFilter = true customBrowserXSSValue = "foobar" contentSecurityPolicy = "foobar" + contentSecurityPolicyReportOnly = "foobar" publicKey = "foobar" referrerPolicy = "foobar" permissionsPolicy = "foobar" diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml index 6f675e626..d0de416ee 100644 --- a/docs/content/reference/dynamic-configuration/file.yaml +++ b/docs/content/reference/dynamic-configuration/file.yaml @@ -242,6 +242,7 @@ http: browserXssFilter: true customBrowserXSSValue: foobar contentSecurityPolicy: foobar + contentSecurityPolicyReportOnly: foobar publicKey: foobar referrerPolicy: foobar permissionsPolicy: foobar diff --git a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml index fa2baaf1e..761a8f928 100644 --- a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml @@ -1309,6 +1309,10 @@ spec: description: ContentSecurityPolicy defines the Content-Security-Policy header value. type: string + contentSecurityPolicyReportOnly: + description: ContentSecurityPolicyReportOnly defines the Content-Security-Policy-Report-Only + header value. + type: string contentTypeNosniff: description: ContentTypeNosniff defines whether to add the X-Content-Type-Options header with the nosniff value. diff --git a/docs/content/reference/dynamic-configuration/kv-ref.md b/docs/content/reference/dynamic-configuration/kv-ref.md index cd425b710..10b9a8d00 100644 --- a/docs/content/reference/dynamic-configuration/kv-ref.md +++ b/docs/content/reference/dynamic-configuration/kv-ref.md @@ -71,6 +71,7 @@ THIS FILE MUST NOT BE EDITED BY HAND | `traefik/http/middlewares/Middleware12/headers/allowedHosts/1` | `foobar` | | `traefik/http/middlewares/Middleware12/headers/browserXssFilter` | `true` | | `traefik/http/middlewares/Middleware12/headers/contentSecurityPolicy` | `foobar` | +| `traefik/http/middlewares/Middleware12/headers/contentSecurityPolicyReportOnly` | `foobar` | | `traefik/http/middlewares/Middleware12/headers/contentTypeNosniff` | `true` | | `traefik/http/middlewares/Middleware12/headers/customBrowserXSSValue` | `foobar` | | `traefik/http/middlewares/Middleware12/headers/customFrameOptionsValue` | `foobar` | diff --git a/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml b/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml index 72b7d69bd..0d005e64d 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml @@ -585,6 +585,10 @@ spec: description: ContentSecurityPolicy defines the Content-Security-Policy header value. type: string + contentSecurityPolicyReportOnly: + description: ContentSecurityPolicyReportOnly defines the Content-Security-Policy-Report-Only + header value. + type: string contentTypeNosniff: description: ContentTypeNosniff defines whether to add the X-Content-Type-Options header with the nosniff value. diff --git a/integration/fixtures/k8s/01-traefik-crd.yml b/integration/fixtures/k8s/01-traefik-crd.yml index fa2baaf1e..761a8f928 100644 --- a/integration/fixtures/k8s/01-traefik-crd.yml +++ b/integration/fixtures/k8s/01-traefik-crd.yml @@ -1309,6 +1309,10 @@ spec: description: ContentSecurityPolicy defines the Content-Security-Policy header value. type: string + contentSecurityPolicyReportOnly: + description: ContentSecurityPolicyReportOnly defines the Content-Security-Policy-Report-Only + header value. + type: string contentTypeNosniff: description: ContentTypeNosniff defines whether to add the X-Content-Type-Options header with the nosniff value. diff --git a/pkg/config/dynamic/fixtures/sample.toml b/pkg/config/dynamic/fixtures/sample.toml index f6ab56cf1..c096381aa 100644 --- a/pkg/config/dynamic/fixtures/sample.toml +++ b/pkg/config/dynamic/fixtures/sample.toml @@ -330,6 +330,7 @@ browserXssFilter = true customBrowserXSSValue = "foobar" contentSecurityPolicy = "foobar" + contentSecurityPolicyReportOnly = "foobar" publicKey = "foobar" referrerPolicy = "foobar" isDevelopment = true diff --git a/pkg/config/dynamic/middlewares.go b/pkg/config/dynamic/middlewares.go index 568c7f46b..4042ed3eb 100644 --- a/pkg/config/dynamic/middlewares.go +++ b/pkg/config/dynamic/middlewares.go @@ -313,6 +313,8 @@ type Headers struct { CustomBrowserXSSValue string `json:"customBrowserXSSValue,omitempty" toml:"customBrowserXSSValue,omitempty" yaml:"customBrowserXSSValue,omitempty"` // ContentSecurityPolicy defines the Content-Security-Policy header value. ContentSecurityPolicy string `json:"contentSecurityPolicy,omitempty" toml:"contentSecurityPolicy,omitempty" yaml:"contentSecurityPolicy,omitempty"` + // ContentSecurityPolicyReportOnly defines the Content-Security-Policy-Report-Only header value. + ContentSecurityPolicyReportOnly string `json:"contentSecurityPolicyReportOnly,omitempty" toml:"contentSecurityPolicyReportOnly,omitempty" yaml:"contentSecurityPolicyReportOnly,omitempty"` // PublicKey is the public key that implements HPKP to prevent MITM attacks with forged certificates. PublicKey string `json:"publicKey,omitempty" toml:"publicKey,omitempty" yaml:"publicKey,omitempty"` // ReferrerPolicy defines the Referrer-Policy header value. @@ -376,6 +378,7 @@ func (h *Headers) HasSecureHeadersDefined() bool { h.BrowserXSSFilter || h.CustomBrowserXSSValue != "" || h.ContentSecurityPolicy != "" || + h.ContentSecurityPolicyReportOnly != "" || h.PublicKey != "" || h.ReferrerPolicy != "" || (h.FeaturePolicy != nil && *h.FeaturePolicy != "") || diff --git a/pkg/config/label/label_test.go b/pkg/config/label/label_test.go index e1c168820..c9989e1e3 100644 --- a/pkg/config/label/label_test.go +++ b/pkg/config/label/label_test.go @@ -63,6 +63,7 @@ func TestDecodeConfiguration(t *testing.T) { "traefik.http.middlewares.Middleware8.headers.addvaryheader": "true", "traefik.http.middlewares.Middleware8.headers.browserxssfilter": "true", "traefik.http.middlewares.Middleware8.headers.contentsecuritypolicy": "foobar", + "traefik.http.middlewares.Middleware8.headers.contentsecuritypolicyreportonly": "foobar", "traefik.http.middlewares.Middleware8.headers.contenttypenosniff": "true", "traefik.http.middlewares.Middleware8.headers.custombrowserxssvalue": "foobar", "traefik.http.middlewares.Middleware8.headers.customframeoptionsvalue": "foobar", @@ -611,22 +612,23 @@ func TestDecodeConfiguration(t *testing.T) { "name0": "foobar", "name1": "foobar", }, - SSLForceHost: Bool(true), - STSSeconds: 42, - STSIncludeSubdomains: true, - STSPreload: true, - ForceSTSHeader: true, - FrameDeny: true, - CustomFrameOptionsValue: "foobar", - ContentTypeNosniff: true, - BrowserXSSFilter: true, - CustomBrowserXSSValue: "foobar", - ContentSecurityPolicy: "foobar", - PublicKey: "foobar", - ReferrerPolicy: "foobar", - FeaturePolicy: String("foobar"), - PermissionsPolicy: "foobar", - IsDevelopment: true, + SSLForceHost: Bool(true), + STSSeconds: 42, + STSIncludeSubdomains: true, + STSPreload: true, + ForceSTSHeader: true, + FrameDeny: true, + CustomFrameOptionsValue: "foobar", + ContentTypeNosniff: true, + BrowserXSSFilter: true, + CustomBrowserXSSValue: "foobar", + ContentSecurityPolicy: "foobar", + ContentSecurityPolicyReportOnly: "foobar", + PublicKey: "foobar", + ReferrerPolicy: "foobar", + FeaturePolicy: String("foobar"), + PermissionsPolicy: "foobar", + IsDevelopment: true, }, }, "Middleware9": { @@ -1134,22 +1136,23 @@ func TestEncodeConfiguration(t *testing.T) { "name0": "foobar", "name1": "foobar", }, - SSLForceHost: Bool(true), - STSSeconds: 42, - STSIncludeSubdomains: true, - STSPreload: true, - ForceSTSHeader: true, - FrameDeny: true, - CustomFrameOptionsValue: "foobar", - ContentTypeNosniff: true, - BrowserXSSFilter: true, - CustomBrowserXSSValue: "foobar", - ContentSecurityPolicy: "foobar", - PublicKey: "foobar", - ReferrerPolicy: "foobar", - FeaturePolicy: String("foobar"), - PermissionsPolicy: "foobar", - IsDevelopment: true, + SSLForceHost: Bool(true), + STSSeconds: 42, + STSIncludeSubdomains: true, + STSPreload: true, + ForceSTSHeader: true, + FrameDeny: true, + CustomFrameOptionsValue: "foobar", + ContentTypeNosniff: true, + BrowserXSSFilter: true, + CustomBrowserXSSValue: "foobar", + ContentSecurityPolicy: "foobar", + ContentSecurityPolicyReportOnly: "foobar", + PublicKey: "foobar", + ReferrerPolicy: "foobar", + FeaturePolicy: String("foobar"), + PermissionsPolicy: "foobar", + IsDevelopment: true, }, }, "Middleware9": { @@ -1299,6 +1302,7 @@ func TestEncodeConfiguration(t *testing.T) { "traefik.HTTP.Middlewares.Middleware8.Headers.AllowedHosts": "foobar, fiibar", "traefik.HTTP.Middlewares.Middleware8.Headers.BrowserXSSFilter": "true", "traefik.HTTP.Middlewares.Middleware8.Headers.ContentSecurityPolicy": "foobar", + "traefik.HTTP.Middlewares.Middleware8.Headers.ContentSecurityPolicyReportOnly": "foobar", "traefik.HTTP.Middlewares.Middleware8.Headers.ContentTypeNosniff": "true", "traefik.HTTP.Middlewares.Middleware8.Headers.CustomBrowserXSSValue": "foobar", "traefik.HTTP.Middlewares.Middleware8.Headers.CustomFrameOptionsValue": "foobar", diff --git a/pkg/middlewares/headers/secure.go b/pkg/middlewares/headers/secure.go index 1766e6356..9769627d1 100644 --- a/pkg/middlewares/headers/secure.go +++ b/pkg/middlewares/headers/secure.go @@ -17,24 +17,25 @@ type secureHeader struct { // newSecure constructs a new secure instance with supplied options. func newSecure(next http.Handler, cfg dynamic.Headers, contextKey string) *secureHeader { opt := secure.Options{ - BrowserXssFilter: cfg.BrowserXSSFilter, - ContentTypeNosniff: cfg.ContentTypeNosniff, - ForceSTSHeader: cfg.ForceSTSHeader, - FrameDeny: cfg.FrameDeny, - IsDevelopment: cfg.IsDevelopment, - STSIncludeSubdomains: cfg.STSIncludeSubdomains, - STSPreload: cfg.STSPreload, - ContentSecurityPolicy: cfg.ContentSecurityPolicy, - CustomBrowserXssValue: cfg.CustomBrowserXSSValue, - CustomFrameOptionsValue: cfg.CustomFrameOptionsValue, - PublicKey: cfg.PublicKey, - ReferrerPolicy: cfg.ReferrerPolicy, - AllowedHosts: cfg.AllowedHosts, - HostsProxyHeaders: cfg.HostsProxyHeaders, - SSLProxyHeaders: cfg.SSLProxyHeaders, - STSSeconds: cfg.STSSeconds, - PermissionsPolicy: cfg.PermissionsPolicy, - SecureContextKey: contextKey, + BrowserXssFilter: cfg.BrowserXSSFilter, + ContentTypeNosniff: cfg.ContentTypeNosniff, + ForceSTSHeader: cfg.ForceSTSHeader, + FrameDeny: cfg.FrameDeny, + IsDevelopment: cfg.IsDevelopment, + STSIncludeSubdomains: cfg.STSIncludeSubdomains, + STSPreload: cfg.STSPreload, + ContentSecurityPolicy: cfg.ContentSecurityPolicy, + ContentSecurityPolicyReportOnly: cfg.ContentSecurityPolicyReportOnly, + CustomBrowserXssValue: cfg.CustomBrowserXSSValue, + CustomFrameOptionsValue: cfg.CustomFrameOptionsValue, + PublicKey: cfg.PublicKey, + ReferrerPolicy: cfg.ReferrerPolicy, + AllowedHosts: cfg.AllowedHosts, + HostsProxyHeaders: cfg.HostsProxyHeaders, + SSLProxyHeaders: cfg.SSLProxyHeaders, + STSSeconds: cfg.STSSeconds, + PermissionsPolicy: cfg.PermissionsPolicy, + SecureContextKey: contextKey, } return &secureHeader{ diff --git a/pkg/provider/kv/kv_test.go b/pkg/provider/kv/kv_test.go index 438871031..679bb258f 100644 --- a/pkg/provider/kv/kv_test.go +++ b/pkg/provider/kv/kv_test.go @@ -139,6 +139,7 @@ func Test_buildConfiguration(t *testing.T) { "traefik/http/middlewares/Middleware09/headers/accessControlExposeHeaders/0": "foobar", "traefik/http/middlewares/Middleware09/headers/accessControlExposeHeaders/1": "foobar", "traefik/http/middlewares/Middleware09/headers/contentSecurityPolicy": "foobar", + "traefik/http/middlewares/Middleware09/headers/contentSecurityPolicyReportOnly": "foobar", "traefik/http/middlewares/Middleware09/headers/publicKey": "foobar", "traefik/http/middlewares/Middleware09/headers/customRequestHeaders/name0": "foobar", "traefik/http/middlewares/Middleware09/headers/customRequestHeaders/name1": "foobar", @@ -601,22 +602,23 @@ func Test_buildConfiguration(t *testing.T) { "name1": "foobar", "name0": "foobar", }, - SSLForceHost: Bool(true), - STSSeconds: 42, - STSIncludeSubdomains: true, - STSPreload: true, - ForceSTSHeader: true, - FrameDeny: true, - CustomFrameOptionsValue: "foobar", - ContentTypeNosniff: true, - BrowserXSSFilter: true, - CustomBrowserXSSValue: "foobar", - ContentSecurityPolicy: "foobar", - PublicKey: "foobar", - ReferrerPolicy: "foobar", - FeaturePolicy: String("foobar"), - PermissionsPolicy: "foobar", - IsDevelopment: true, + SSLForceHost: Bool(true), + STSSeconds: 42, + STSIncludeSubdomains: true, + STSPreload: true, + ForceSTSHeader: true, + FrameDeny: true, + CustomFrameOptionsValue: "foobar", + ContentTypeNosniff: true, + BrowserXSSFilter: true, + CustomBrowserXSSValue: "foobar", + ContentSecurityPolicy: "foobar", + ContentSecurityPolicyReportOnly: "foobar", + PublicKey: "foobar", + ReferrerPolicy: "foobar", + FeaturePolicy: String("foobar"), + PermissionsPolicy: "foobar", + IsDevelopment: true, }, }, "Middleware17": { diff --git a/pkg/redactor/redactor_config_test.go b/pkg/redactor/redactor_config_test.go index ed498721f..ee653b28a 100644 --- a/pkg/redactor/redactor_config_test.go +++ b/pkg/redactor/redactor_config_test.go @@ -214,6 +214,7 @@ func init() { BrowserXSSFilter: true, CustomBrowserXSSValue: "foo", ContentSecurityPolicy: "foo", + ContentSecurityPolicyReportOnly: "foo", PublicKey: "foo", ReferrerPolicy: "foo", PermissionsPolicy: "foo", diff --git a/pkg/redactor/testdata/anonymized-dynamic-config.json b/pkg/redactor/testdata/anonymized-dynamic-config.json index b4afd7aa1..ed3c07c86 100644 --- a/pkg/redactor/testdata/anonymized-dynamic-config.json +++ b/pkg/redactor/testdata/anonymized-dynamic-config.json @@ -170,6 +170,7 @@ "browserXssFilter": true, "customBrowserXSSValue": "xxxx", "contentSecurityPolicy": "xxxx", + "contentSecurityPolicyReportOnly": "xxxx", "publicKey": "xxxx", "referrerPolicy": "foo", "permissionsPolicy": "foo", diff --git a/pkg/redactor/testdata/secured-dynamic-config.json b/pkg/redactor/testdata/secured-dynamic-config.json index 8ff3d0789..75c70ae25 100644 --- a/pkg/redactor/testdata/secured-dynamic-config.json +++ b/pkg/redactor/testdata/secured-dynamic-config.json @@ -173,6 +173,7 @@ "browserXssFilter": true, "customBrowserXSSValue": "foo", "contentSecurityPolicy": "foo", + "contentSecurityPolicyReportOnly": "foo", "publicKey": "foo", "referrerPolicy": "foo", "permissionsPolicy": "foo", diff --git a/webui/src/components/_commons/PanelMiddlewares.vue b/webui/src/components/_commons/PanelMiddlewares.vue index 98eff915a..4921a0b54 100644 --- a/webui/src/components/_commons/PanelMiddlewares.vue +++ b/webui/src/components/_commons/PanelMiddlewares.vue @@ -817,6 +817,22 @@ + + +
+
+
+ Content Security Policy (Report Only) +
+ + {{ exData(middleware).contentSecurityPolicyReportOnly }} + +
+
+