diff options
| author | Rob Hudson <rob@cogit8.org> | 2025-08-23 12:23:53 -0700 |
|---|---|---|
| committer | nessita <124304+nessita@users.noreply.github.com> | 2025-08-28 17:23:48 -0300 |
| commit | 550822bceea227b07445d1852c4376b663c09ea4 (patch) | |
| tree | c7b9a032939c7611156b54767c10059d7d6e373c /django | |
| parent | 292b9e6fe8f23491680d9cc60f328562e2b1c823 (diff) | |
Fixed #36532 -- Added Content Security Policy view decorators to override or disable policies.
Co-authored-by: Natalia <124304+nessita@users.noreply.github.com>
Diffstat (limited to 'django')
| -rw-r--r-- | django/middleware/csp.py | 23 | ||||
| -rw-r--r-- | django/views/debug.py | 5 | ||||
| -rw-r--r-- | django/views/decorators/csp.py | 39 |
3 files changed, 54 insertions, 13 deletions
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) |
