diff --git a/filter_plugins/csp_filters.py b/filter_plugins/csp_filters.py index 23b71dc0..48dfc8d6 100644 --- a/filter_plugins/csp_filters.py +++ b/filter_plugins/csp_filters.py @@ -5,6 +5,7 @@ import sys, os sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) from module_utils.config_utils import get_app_conf +from module_utils.get_url import get_url class FilterModule(object): """ @@ -131,17 +132,14 @@ class FilterModule(object): flags = self.get_csp_flags(applications, application_id, directive) tokens += flags + if directive in [ 'script-src-elem', 'connect-src', 'style-src-elem' ]: + # Allow fetching from internal CDN as default for all applications + tokens.append(get_url(domains,'web-svc-cdn',web_protocol)) if directive in ['script-src-elem', 'connect-src']: # Matomo integration if self.is_feature_enabled(applications, matomo_feature_name, application_id): - matomo_domain = domains.get('web-app-matomo')[0] - if matomo_domain: - tokens.append(f"{web_protocol}://{matomo_domain}") - - # Allow fetching from internal CDN as default for all applications - domain = domains.get('web-svc-cdn')[0] - tokens.append(f"{web_protocol}://{domain}") + tokens.append(get_url(domains,'web-app-matomo',web_protocol)) # ReCaptcha integration: allow loading scripts from Google if feature enabled if self.is_feature_enabled(applications, 'recaptcha', application_id): @@ -159,13 +157,11 @@ class FilterModule(object): if self.is_feature_enabled(applications, 'logout', application_id): # Allow logout via infinito logout proxy - domain = domains.get('web-svc-logout')[0] - tokens.append(f"{web_protocol}://{domain}") + tokens.append(get_url(domains,'web-svc-logout',web_protocol)) # Allow logout via keycloak app - domain = domains.get('web-app-keycloak')[0] - tokens.append(f"{web_protocol}://{domain}") - + tokens.append(get_url(domains,'web-app-keycloak',web_protocol)) + # whitelist tokens += self.get_csp_whitelist(applications, application_id, directive) diff --git a/tests/unit/filter_plugins/test_csp_filters.py b/tests/unit/filter_plugins/test_csp_filters.py index 1d0e86a6..0b231831 100644 --- a/tests/unit/filter_plugins/test_csp_filters.py +++ b/tests/unit/filter_plugins/test_csp_filters.py @@ -55,6 +55,28 @@ class TestCspFilters(unittest.TestCase): 'web-svc-cdn': ['cdn.example.org'], } + # --- Helpers ------------------------------------------------------------- + + def _get_directive_tokens(self, header: str, directive: str): + """ + Extract tokens (as a list of strings) for a given directive from a CSP header. + Example: for "connect-src 'self' https://a https://b;" -> ["'self'", "https://a", "https://b"] + Returns [] if not found. + """ + for part in header.split(';'): + part = part.strip() + if not part: + continue + if part.startswith(directive + ' '): + # remove directive name, split remainder by spaces + remainder = part[len(directive):].strip() + return [tok for tok in remainder.split(' ') if tok] + if part == directive: # unlikely, but guard + return [] + return [] + + # --- Tests --------------------------------------------------------------- + def test_get_csp_whitelist_list(self): result = self.filter.get_csp_whitelist(self.apps, 'app1', 'script-src-elem') self.assertEqual(result, ['https://cdn.example.com']) @@ -85,18 +107,23 @@ class TestCspFilters(unittest.TestCase): header = self.filter.build_csp_header(self.apps, 'app1', self.domains, web_protocol='https') # Ensure core directives are present self.assertIn("default-src 'self';", header) + # script-src-elem should include 'self', Matomo, internes CDN und explizite Whitelist-CDN self.assertIn("script-src-elem 'self'", header) self.assertIn("https://matomo.example.org", header) self.assertIn("https://cdn.example.org", header) # internes CDN self.assertIn("https://cdn.example.com", header) # Whitelist + # script-src directive should include unsafe-eval self.assertIn("script-src 'self' 'unsafe-eval'", header) - # connect-src directive - self.assertIn( - "connect-src 'self' https://matomo.example.org https://cdn.example.org https://api.example.com;", - header - ) + + # connect-src directive (reihenfolgeunabhängig prüfen) + tokens = self._get_directive_tokens(header, "connect-src") + self.assertIn("'self'", tokens) + self.assertIn("https://matomo.example.org", tokens) + self.assertIn("https://cdn.example.org", tokens) + self.assertIn("https://api.example.com", tokens) + # ends with img-src self.assertTrue(header.strip().endswith('img-src * data: blob:;'))