From 3f8e7c17334aa4a19cb1d2b69b94a45836431c11 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Mon, 1 Sep 2025 09:03:22 +0200 Subject: [PATCH] Refactor CSP filter: - Move default 'unsafe-inline' for style-src and style-src-elem into get_csp_flags - Ensure hashes are only added if 'unsafe-inline' not in final tokens - Improve comments and structure - Extend unit tests to cover default flags, overrides, and final-token logic See: https://chatgpt.com/share/68b54520-5cfc-800f-9bac-45093740df78 --- filter_plugins/csp_filters.py | 127 +++++++++++------- tests/unit/filter_plugins/test_csp_filters.py | 100 ++++++++++++++ 2 files changed, 178 insertions(+), 49 deletions(-) diff --git a/filter_plugins/csp_filters.py b/filter_plugins/csp_filters.py index 74965235..a0af0180 100644 --- a/filter_plugins/csp_filters.py +++ b/filter_plugins/csp_filters.py @@ -1,12 +1,15 @@ from ansible.errors import AnsibleFilterError import hashlib import base64 -import sys, os +import sys +import os +# Ensure module_utils is importable when this filter runs from Ansible 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): """ Custom filters for Content Security Policy generation and CSP-related utilities. @@ -17,10 +20,14 @@ class FilterModule(object): 'build_csp_header': self.build_csp_header, } + # ------------------------------- + # Helpers + # ------------------------------- + @staticmethod def is_feature_enabled(applications: dict, feature: str, application_id: str) -> bool: """ - Return True if applications[application_id].features[feature] is truthy. + Returns True if applications[application_id].features[feature] is truthy. """ return get_app_conf( applications, @@ -32,6 +39,10 @@ class FilterModule(object): @staticmethod def get_csp_whitelist(applications, application_id, directive): + """ + Returns a list of additional whitelist entries for a given directive. + Accepts both scalar and list in config; always returns a list. + """ wl = get_app_conf( applications, application_id, @@ -48,28 +59,37 @@ class FilterModule(object): @staticmethod def get_csp_flags(applications, application_id, directive): """ - Dynamically extract all CSP flags for a given directive and return them as tokens, - e.g., "'unsafe-eval'", "'unsafe-inline'", etc. + Returns CSP flag tokens (e.g., "'unsafe-eval'", "'unsafe-inline'") for a directive, + merging sane defaults with app config. + Default: 'unsafe-inline' is enabled for style-src and style-src-elem. """ - flags = get_app_conf( + # Defaults that apply to all apps + default_flags = {} + if directive in ('style-src', 'style-src-elem'): + default_flags = {'unsafe-inline': True} + + configured = get_app_conf( applications, application_id, 'server.csp.flags.' + directive, False, {} ) - tokens = [] - for flag_name, enabled in flags.items(): + # Merge defaults with configured flags (configured overrides defaults) + merged = {**default_flags, **configured} + + tokens = [] + for flag_name, enabled in merged.items(): if enabled: tokens.append(f"'{flag_name}'") - return tokens @staticmethod def get_csp_inline_content(applications, application_id, directive): """ - Return inline script/style snippets to hash for a given CSP directive. + Returns inline script/style snippets to hash for a given directive. + Accepts both scalar and list in config; always returns a list. """ snippets = get_app_conf( applications, @@ -87,7 +107,7 @@ class FilterModule(object): @staticmethod def get_csp_hash(content): """ - Compute the SHA256 hash of the given inline content and return + Computes the SHA256 hash of the given inline content and returns a CSP token like "'sha256-'". """ try: @@ -97,6 +117,10 @@ class FilterModule(object): except Exception as exc: raise AnsibleFilterError(f"get_csp_hash failed: {exc}") + # ------------------------------- + # Main builder + # ------------------------------- + def build_csp_header( self, applications, @@ -106,75 +130,80 @@ class FilterModule(object): matomo_feature_name='matomo' ): """ - Build the Content-Security-Policy header value dynamically based on application settings. - Inline hashes are read from applications[application_id].csp.hashes + Builds the Content-Security-Policy header value dynamically based on application settings. + - Flags (e.g., 'unsafe-eval', 'unsafe-inline') are read from server.csp.flags., + with sane defaults applied in get_csp_flags (always 'unsafe-inline' for style-src and style-src-elem). + - Inline hashes are read from server.csp.hashes.. + - Whitelists are read from server.csp.whitelist.. + - Inline hashes are added only if the final tokens do NOT include 'unsafe-inline'. """ try: directives = [ - 'default-src', # Fallback source list for all content types not explicitly listed - 'connect-src', # Controls allowed URLs for XHR, WebSockets, EventSource, and fetch() - 'frame-ancestors', # Restricts which parent frames can embed this page via