Add support for Content-Security-Policy header

Adding a configuration parameter csp-options that, when set, adds a
Content-Security-Policy header to the apache configuration.
This header can prevent or minimize the risk of certain types of
security threats by placing restrictions on the things the web page's
code can do.

Closes-Bug: #2118835

Change-Id: I06f0b1c2787fa56460e5a196d3ca07c0a85c14e3
Signed-off-by: Jorge Merlino <jorge.merlino@canonical.com>
This commit is contained in:
Jorge Merlino
2025-07-26 17:58:07 -03:00
parent 4832954286
commit 41250d97d1
5 changed files with 63 additions and 20 deletions

View File

@@ -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

View File

@@ -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'):

View File

@@ -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 %}
</VirtualHost>

View File

@@ -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"

View File

@@ -74,39 +74,68 @@ class TestHorizonContexts(CharmTestCase):
self.pwgen.return_value = "secret"
def test_Apachecontext(self):
self.assertEqual(horizon_contexts.ApacheContext()(),
self.assertEqual(
horizon_contexts.ApacheContext()(),
{'http_port': 70, 'https_port': 433,
'enforce_ssl': False,
'hsts_max_age_seconds': 0,
'custom_theme': False})
'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()(),
self.assertEqual(
horizon_contexts.ApacheContext()(),
{'http_port': 70, 'https_port': 433,
'enforce_ssl': True,
'hsts_max_age_seconds': 0,
'custom_theme': False})
'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()(),
self.assertEqual(
horizon_contexts.ApacheContext()(),
{'http_port': 70, 'https_port': 433,
'enforce_ssl': False,
'hsts_max_age_seconds': 0,
'custom_theme': False})
'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()(),
self.assertEqual(
horizon_contexts.ApacheContext()(),
{'http_port': 70, 'https_port': 433,
'enforce_ssl': True,
'hsts_max_age_seconds': 15768000,
'custom_theme': False})
'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()(),