Content Security Policy (CSP) enforcement / XSS mitigation

HIGH
django/django
Commit: ff0ff98d4279
Affected: stable/5.1.x
2026-04-05 13:09 UTC

Description

The commit replaces a custom CSP middleware used in the Django admin Selenium tests with Django's official ContentSecurityPolicyMiddleware and configures SECURE_CSP to a strict policy. It also adds a test assertion to verify that no CSP violations are emitted in browser console logs. This is a defensive hardening fix addressing content security policy handling to reduce the risk of XSS in the admin interface stemming from misconfigurations in a custom CSP middleware, aligning admin CSP behavior with Django's built-in CSP support.

Commit Details

Author: Natalia

Date: 2025-06-25 20:22 UTC

Message:

Refs #15727 -- Updated AdminSeleniumTestCase to use ContentSecurityPolicyMiddleware. Replaced the custom CSP middleware previously used in the admin's AdminSeleniumTestCase with the official ContentSecurityPolicyMiddleware. This change ensures alignment with Django's built-in CSP support. Also updates the test logic to inspect browser console logs to assert that no CSP violations are triggered during Selenium admin tests.

Triage Assessment

Vulnerability Type: Content Security Policy / XSS

Confidence: HIGH

Reasoning:

The commit replaces a custom CSP middleware with Django's official ContentSecurityPolicyMiddleware and updates tests to verify CSP behavior. CSP helps prevent Cross-Site Scripting (XSS) and related content injection by enforcing allowed sources. Aligning with built-in CSP support and adding validation that no CSP violations occur during admin Selenium tests indicates a security hardening fix.

Verification Assessment

Vulnerability Type: Content Security Policy (CSP) enforcement / XSS mitigation

Confidence: HIGH

Affected Versions: stable/5.1.x

Code Diff

diff --git a/django/contrib/admin/tests.py b/django/contrib/admin/tests.py index f64a4c47f81a..3982142330a8 100644 --- a/django/contrib/admin/tests.py +++ b/django/contrib/admin/tests.py @@ -1,24 +1,27 @@ from contextlib import contextmanager from django.contrib.staticfiles.testing import StaticLiveServerTestCase -from django.test import modify_settings +from django.test import modify_settings, override_settings from django.test.selenium import SeleniumTestCase -from django.utils.deprecation import MiddlewareMixin +from django.utils.csp import CSP from django.utils.translation import gettext as _ # Make unittest ignore frames in this module when reporting failures. __unittest = True -class CSPMiddleware(MiddlewareMixin): - """The admin's JavaScript should be compatible with CSP.""" - - def process_response(self, request, response): - response.headers["Content-Security-Policy"] = "default-src 'self'" - return response - - -@modify_settings(MIDDLEWARE={"append": "django.contrib.admin.tests.CSPMiddleware"}) +@modify_settings( + MIDDLEWARE={"append": "django.middleware.csp.ContentSecurityPolicyMiddleware"} +) +@override_settings( + SECURE_CSP={ + "default-src": [CSP.NONE], + "connect-src": [CSP.SELF], + "img-src": [CSP.SELF], + "script-src": [CSP.SELF], + "style-src": [CSP.SELF], + }, +) class AdminSeleniumTestCase(SeleniumTestCase, StaticLiveServerTestCase): available_apps = [ "django.contrib.admin", @@ -28,6 +31,11 @@ class AdminSeleniumTestCase(SeleniumTestCase, StaticLiveServerTestCase): "django.contrib.sites", ] + def tearDown(self): + # Ensure that no CSP violations were logged in the browser. + self.assertEqual(self.get_browser_logs(source="security"), []) + super().tearDown() + def wait_until(self, callback, timeout=10): """ Block the execution of the tests until the specified callback returns a diff --git a/django/test/selenium.py b/django/test/selenium.py index 15ee3002eca4..be8f4a815f00 100644 --- a/django/test/selenium.py +++ b/django/test/selenium.py @@ -77,7 +77,11 @@ def import_options(cls, browser): def get_capability(cls, browser): from selenium.webdriver.common.desired_capabilities import DesiredCapabilities - return getattr(DesiredCapabilities, browser.upper()) + caps = getattr(DesiredCapabilities, browser.upper()) + if browser == "chrome": + caps["goog:loggingPrefs"] = {"browser": "ALL"} + + return caps def create_options(self): options = self.import_options(self.browser)() @@ -237,6 +241,19 @@ def take_screenshot(self, name): path.parent.mkdir(exist_ok=True, parents=True) self.selenium.save_screenshot(path) + def get_browser_logs(self, source=None, level="ALL"): + """Return Chrome console logs filtered by level and optionally source.""" + try: + logs = self.selenium.get_log("browser") + except AttributeError: + logs = [] + return [ + log + for log in logs + if (level == "ALL" or log["level"] == level) + and (source is None or log["source"] == source) + ] + @classmethod def _quit_selenium(cls): # quit() the WebDriver before attempting to terminate and join the
← Back to Alerts View on GitHub →