From 8e34187b744d2a718c008e5dfef3dd5b917f12ed Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Thu, 20 Feb 2025 12:01:29 -0500 Subject: [PATCH] CSP: add helper middleware (#12004) Sometimes we need to add additional CSP headers to views from third party apps, this middleware helps with that, instead of subclassing and overriding URLs. This will mainly be used on .com. --- readthedocs/core/middleware.py | 44 ++++++++++++++++++++++++++++++++++ readthedocs/settings/base.py | 2 ++ 2 files changed, 46 insertions(+) diff --git a/readthedocs/core/middleware.py b/readthedocs/core/middleware.py index 462b97caea6..4e7e314a25e 100644 --- a/readthedocs/core/middleware.py +++ b/readthedocs/core/middleware.py @@ -1,4 +1,5 @@ import structlog +from django.conf import settings from django.http import HttpResponse log = structlog.get_logger(__name__) @@ -33,3 +34,46 @@ def __call__(self, request): status=400, ) return self.get_response(request) + + +class UpdateCSPMiddleware: + """ + Middleware to update the CSP headers for specific views given its URL name. + + This is useful for views that we don't have much control over, + like views from third-party packages. For views that we have control over, + we should update the CSP headers directly in the view. + + Use the `RTD_CSP_UPDATE_HEADERS` setting to define the views that need to + update the CSP headers. The setting should be a dictionary where the key is + the URL name of the view and the value is a dictionary with the CSP headers, + for example: + + .. code-block:: python + + RTD_CSP_UPDATE_HEADERS = { + "login": {"form-action": ["https:"]}, + } + """ + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + response = self.get_response(request) + + # Views that raised an exception don't have a resolver_match object. + resolver_match = request.resolver_match + if not resolver_match: + return response + + url_name = resolver_match.url_name + update_csp_headers = settings.RTD_CSP_UPDATE_HEADERS + if settings.RTD_EXT_THEME_ENABLED and url_name in update_csp_headers: + if hasattr(response, "_csp_update"): + raise ValueError( + "Can't update CSP headers at the view and middleware at the same time, use one or the other." + ) + response._csp_update = update_csp_headers[url_name] + + return response diff --git a/readthedocs/settings/base.py b/readthedocs/settings/base.py index b05290526e4..1431137f11c 100644 --- a/readthedocs/settings/base.py +++ b/readthedocs/settings/base.py @@ -143,6 +143,7 @@ def SWITCH_PRODUCTION_DOMAIN(self): CSP_REPORT_URI = None CSP_REPORT_ONLY = False CSP_EXCLUDE_URL_PREFIXES = ("/admin/",) + RTD_CSP_UPDATE_HEADERS = {} # Read the Docs READ_THE_DOCS_EXTENSIONS = ext @@ -349,6 +350,7 @@ def MIDDLEWARE(self): "allauth.account.middleware.AccountMiddleware", "dj_pagination.middleware.PaginationMiddleware", "csp.middleware.CSPMiddleware", + "readthedocs.core.middleware.UpdateCSPMiddleware", "simple_history.middleware.HistoryRequestMiddleware", "readthedocs.core.logs.ReadTheDocsRequestMiddleware", "django_structlog.middlewares.CeleryMiddleware",