From 550822bceea227b07445d1852c4376b663c09ea4 Mon Sep 17 00:00:00 2001 From: Rob Hudson Date: Sat, 23 Aug 2025 12:23:53 -0700 Subject: Fixed #36532 -- Added Content Security Policy view decorators to override or disable policies. Co-authored-by: Natalia <124304+nessita@users.noreply.github.com> --- django/middleware/csp.py | 23 ++++++++++------------- django/views/debug.py | 5 +++++ django/views/decorators/csp.py | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 13 deletions(-) create mode 100644 django/views/decorators/csp.py (limited to 'django') diff --git a/django/middleware/csp.py b/django/middleware/csp.py index e1c66ada5a..ba08cfff0c 100644 --- a/django/middleware/csp.py +++ b/django/middleware/csp.py @@ -1,5 +1,3 @@ -from http import HTTPStatus - from django.conf import settings from django.utils.csp import CSP, LazyNonce, build_policy from django.utils.deprecation import MiddlewareMixin @@ -14,22 +12,21 @@ class ContentSecurityPolicyMiddleware(MiddlewareMixin): request._csp_nonce = LazyNonce() def process_response(self, request, response): - # In DEBUG mode, exclude CSP headers for specific status codes that - # trigger the debug view. - exempted_status_codes = { - HTTPStatus.NOT_FOUND, - HTTPStatus.INTERNAL_SERVER_ERROR, - } - if settings.DEBUG and response.status_code in exempted_status_codes: - return response - nonce = get_nonce(request) + + sentinel = object() + if (csp_config := getattr(response, "_csp_config", sentinel)) is sentinel: + csp_config = settings.SECURE_CSP + if (csp_ro_config := getattr(response, "_csp_ro_config", sentinel)) is sentinel: + csp_ro_config = settings.SECURE_CSP_REPORT_ONLY + for header, config in [ - (CSP.HEADER_ENFORCE, settings.SECURE_CSP), - (CSP.HEADER_REPORT_ONLY, settings.SECURE_CSP_REPORT_ONLY), + (CSP.HEADER_ENFORCE, csp_config), + (CSP.HEADER_REPORT_ONLY, csp_ro_config), ]: # If headers are already set on the response, don't overwrite them. # This allows for views to set their own CSP headers as needed. + # An empty config means CSP headers are not added to the response. if config and header not in response: response.headers[str(header)] = build_policy(config, nonce) diff --git a/django/views/debug.py b/django/views/debug.py index 75f30ca601..5a1b4aee91 100644 --- a/django/views/debug.py +++ b/django/views/debug.py @@ -18,6 +18,7 @@ from django.utils.encoding import force_str from django.utils.module_loading import import_string from django.utils.regex_helper import _lazy_re_compile from django.utils.version import get_docs_version +from django.views.decorators.csp import csp_override, csp_report_only_override from django.views.decorators.debug import coroutine_functions_to_sensitive_variables # Minimal Django templates engine to render the error templates @@ -59,6 +60,8 @@ class CallableSettingWrapper: return repr(self._wrapped) +@csp_override({}) +@csp_report_only_override({}) def technical_500_response(request, exc_type, exc_value, tb, status_code=500): """ Create a technical server error response. The last three arguments are @@ -606,6 +609,8 @@ class ExceptionReporter: tb = tb.tb_next +@csp_override({}) +@csp_report_only_override({}) def technical_404_response(request, exception): """Create a technical 404 error response. `exception` is the Http404.""" try: diff --git a/django/views/decorators/csp.py b/django/views/decorators/csp.py new file mode 100644 index 0000000000..9033d9cdf0 --- /dev/null +++ b/django/views/decorators/csp.py @@ -0,0 +1,39 @@ +from functools import wraps + +from asgiref.sync import iscoroutinefunction + + +def _make_csp_decorator(config_attr_name, config_attr_value): + """General CSP override decorator factory.""" + + if not isinstance(config_attr_value, dict): + raise TypeError("CSP config should be a mapping.") + + def decorator(view_func): + @wraps(view_func) + async def _wrapped_async_view(request, *args, **kwargs): + response = await view_func(request, *args, **kwargs) + setattr(response, config_attr_name, config_attr_value) + return response + + @wraps(view_func) + def _wrapped_sync_view(request, *args, **kwargs): + response = view_func(request, *args, **kwargs) + setattr(response, config_attr_name, config_attr_value) + return response + + if iscoroutinefunction(view_func): + return _wrapped_async_view + return _wrapped_sync_view + + return decorator + + +def csp_override(config): + """Override the Content-Security-Policy header for a view.""" + return _make_csp_decorator("_csp_config", config) + + +def csp_report_only_override(config): + """Override the Content-Security-Policy-Report-Only header for a view.""" + return _make_csp_decorator("_csp_ro_config", config) -- cgit v1.3