Skip to content

Commit

Permalink
CSP: add helper middleware (#12004)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
stsewd authored Feb 20, 2025
1 parent ade2e8f commit 8e34187
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 0 deletions.
44 changes: 44 additions & 0 deletions readthedocs/core/middleware.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import structlog
from django.conf import settings
from django.http import HttpResponse

log = structlog.get_logger(__name__)
Expand Down Expand Up @@ -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
2 changes: 2 additions & 0 deletions readthedocs/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down

0 comments on commit 8e34187

Please sign in to comment.