diff --git a/config.yaml b/config.yaml index 87121d07..f3176f10 100644 --- a/config.yaml +++ b/config.yaml @@ -331,6 +331,13 @@ options: . For this option to have an effect, SSL must be configured and enforce-ssl option must be true. + csp-options: + type: string + default: "frame-ancestors 'self'; form-action 'self';" + description: | + Options for the CSP (Content Security Policy) header. This header allows to + control which resources the user agent is allowed to load. For more details + on CSP refer to: https://developer.mozilla.org/docs/Web/HTTP/Guides/CSP database-user: type: string default: horizon diff --git a/hooks/horizon_contexts.py b/hooks/horizon_contexts.py index e3a7f7dd..b78f786f 100644 --- a/hooks/horizon_contexts.py +++ b/hooks/horizon_contexts.py @@ -345,6 +345,7 @@ class ApacheContext(OSContextGenerator): 'enforce_ssl': False, 'hsts_max_age_seconds': config('hsts-max-age-seconds'), "custom_theme": config('custom-theme'), + 'csp_options': config('csp-options'), } if config('enforce-ssl'): diff --git a/templates/default b/templates/default index f5cac563..8bffebb7 100644 --- a/templates/default +++ b/templates/default @@ -37,4 +37,7 @@ KeepAliveTimeout 75 MaxKeepAliveRequests 1000 Header set X-Frame-Options: "sameorigin" +{% if csp_options %} + Header set Content-Security-Policy "{{ csp_options }}" +{% endif %} diff --git a/templates/default-ssl b/templates/default-ssl index e34e2b19..b3983ea6 100644 --- a/templates/default-ssl +++ b/templates/default-ssl @@ -52,6 +52,9 @@ NameVirtualHost *:{{ 443 }} Header set Strict-Transport-Security "max-age={{ hsts_max_age_seconds }}" # NOTE(ajkavanagh) due to Bug 1853173 the cookie can't be secure at this time, so disabling until a fix is found. # Header edit Set-Cookie ^(.*)$ $1;HttpOnly;Secure +{% endif %} +{% if csp_options %} + Header set Content-Security-Policy "{{ csp_options }}" {% endif %} Header set X-XSS-Protection "1; mode=block" Header set X-Content-Type-Options "nosniff" diff --git a/unit_tests/test_horizon_contexts.py b/unit_tests/test_horizon_contexts.py index dd6fc923..231258ff 100644 --- a/unit_tests/test_horizon_contexts.py +++ b/unit_tests/test_horizon_contexts.py @@ -74,39 +74,68 @@ class TestHorizonContexts(CharmTestCase): self.pwgen.return_value = "secret" def test_Apachecontext(self): - self.assertEqual(horizon_contexts.ApacheContext()(), - {'http_port': 70, 'https_port': 433, - 'enforce_ssl': False, - 'hsts_max_age_seconds': 0, - 'custom_theme': False}) + self.assertEqual( + horizon_contexts.ApacheContext()(), + {'http_port': 70, 'https_port': 433, + 'enforce_ssl': False, + 'hsts_max_age_seconds': 0, + 'csp_options': "frame-ancestors 'self'; form-action 'self';", + 'custom_theme': False}, + ) def test_Apachecontext_enforce_ssl(self): self.test_config.set('enforce-ssl', True) self.https.return_value = True - self.assertEqual(horizon_contexts.ApacheContext()(), - {'http_port': 70, 'https_port': 433, - 'enforce_ssl': True, - 'hsts_max_age_seconds': 0, - 'custom_theme': False}) + self.assertEqual( + horizon_contexts.ApacheContext()(), + {'http_port': 70, 'https_port': 433, + 'enforce_ssl': True, + 'hsts_max_age_seconds': 0, + 'csp_options': "frame-ancestors 'self'; form-action 'self';", + 'custom_theme': False}, + ) def test_Apachecontext_enforce_ssl_no_cert(self): self.test_config.set('enforce-ssl', True) self.https.return_value = False - self.assertEqual(horizon_contexts.ApacheContext()(), - {'http_port': 70, 'https_port': 433, - 'enforce_ssl': False, - 'hsts_max_age_seconds': 0, - 'custom_theme': False}) + self.assertEqual( + horizon_contexts.ApacheContext()(), + {'http_port': 70, 'https_port': 433, + 'enforce_ssl': False, + 'hsts_max_age_seconds': 0, + 'csp_options': "frame-ancestors 'self'; form-action 'self';", + 'custom_theme': False}, + ) def test_Apachecontext_hsts_max_age_seconds(self): self.test_config.set('enforce-ssl', True) self.https.return_value = True self.test_config.set('hsts-max-age-seconds', 15768000) - self.assertEqual(horizon_contexts.ApacheContext()(), - {'http_port': 70, 'https_port': 433, - 'enforce_ssl': True, - 'hsts_max_age_seconds': 15768000, - 'custom_theme': False}) + self.assertEqual( + horizon_contexts.ApacheContext()(), + {'http_port': 70, 'https_port': 433, + 'enforce_ssl': True, + 'hsts_max_age_seconds': 15768000, + 'csp_options': "frame-ancestors 'self'; form-action 'self';", + 'custom_theme': False}, + ) + + def test_Apachecontext_csp_options(self): + self.https.return_value = True + self.test_config.set( + 'csp-options', + "default-src https: 'unsafe-eval'; object-src 'none'", + ) + self.assertEqual( + horizon_contexts.ApacheContext()(), + {'http_port': 70, + 'https_port': 433, + 'enforce_ssl': False, + 'hsts_max_age_seconds': 0, + 'csp_options': + "default-src https: 'unsafe-eval'; object-src 'none'", + 'custom_theme': False}, + ) def test_HorizonContext_defaults(self): self.assertEqual(horizon_contexts.HorizonContext()(),