1
0
mirror of https://github.com/django/django.git synced 2025-07-03 01:09:12 +00:00
django/tests/middleware/test_csp.py
Rob Hudson d63241ebc7 Fixed #15727 -- Added Content Security Policy (CSP) support.
This initial work adds a pair of settings to configure specific CSP
directives for enforcing or reporting policy violations, a new
`django.middleware.csp.ContentSecurityPolicyMiddleware` to apply the
appropriate headers to responses, and a context processor to support CSP
nonces in templates for safely inlining assets.

Relevant documentation has been added for the 6.0 release notes,
security overview, a new how-to page, and a dedicated reference section.

Thanks to the multiple reviewers for their precise and valuable feedback.

Co-authored-by: Natalia <124304+nessita@users.noreply.github.com>
2025-06-27 15:57:02 -03:00

136 lines
4.8 KiB
Python

import time
from utils_tests.test_csp import basic_config, basic_policy
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from django.test import SimpleTestCase
from django.test.selenium import SeleniumTestCase
from django.test.utils import modify_settings, override_settings
from django.utils.csp import CSP
from .views import csp_reports
@override_settings(
MIDDLEWARE=["django.middleware.csp.ContentSecurityPolicyMiddleware"],
ROOT_URLCONF="middleware.urls",
)
class CSPMiddlewareTest(SimpleTestCase):
@override_settings(SECURE_CSP=None, SECURE_CSP_REPORT_ONLY=None)
def test_csp_defaults_off(self):
response = self.client.get("/csp-base/")
self.assertNotIn(CSP.HEADER_ENFORCE, response)
self.assertNotIn(CSP.HEADER_REPORT_ONLY, response)
@override_settings(SECURE_CSP=basic_config, SECURE_CSP_REPORT_ONLY=None)
def test_csp_basic(self):
"""
With SECURE_CSP set to a valid value, the middleware adds a
"Content-Security-Policy" header to the response.
"""
response = self.client.get("/csp-base/")
self.assertEqual(response[CSP.HEADER_ENFORCE], basic_policy)
self.assertNotIn(CSP.HEADER_REPORT_ONLY, response)
@override_settings(SECURE_CSP={"default-src": [CSP.SELF, CSP.NONCE]})
def test_csp_basic_with_nonce(self):
"""
Test the nonce is added to the header and matches what is in the view.
"""
response = self.client.get("/csp-nonce/")
nonce = response.text
self.assertTrue(nonce)
self.assertEqual(
response[CSP.HEADER_ENFORCE], f"default-src 'self' 'nonce-{nonce}'"
)
@override_settings(SECURE_CSP={"default-src": [CSP.SELF, CSP.NONCE]})
def test_csp_basic_with_nonce_but_unused(self):
"""
Test if `request.csp_nonce` is never accessed, it is not added to the header.
"""
response = self.client.get("/csp-base/")
nonce = response.text
self.assertIsNotNone(nonce)
self.assertEqual(response[CSP.HEADER_ENFORCE], basic_policy)
@override_settings(SECURE_CSP=None, SECURE_CSP_REPORT_ONLY=basic_config)
def test_csp_report_only_basic(self):
"""
With SECURE_CSP_REPORT_ONLY set to a valid value, the middleware adds a
"Content-Security-Policy-Report-Only" header to the response.
"""
response = self.client.get("/csp-base/")
self.assertEqual(response[CSP.HEADER_REPORT_ONLY], basic_policy)
self.assertNotIn(CSP.HEADER_ENFORCE, response)
@override_settings(
SECURE_CSP=basic_config,
SECURE_CSP_REPORT_ONLY=basic_config,
)
def test_csp_both(self):
"""
If both SECURE_CSP and SECURE_CSP_REPORT_ONLY are set, the middleware
adds both headers to the response.
"""
response = self.client.get("/csp-base/")
self.assertEqual(response[CSP.HEADER_ENFORCE], basic_policy)
self.assertEqual(response[CSP.HEADER_REPORT_ONLY], basic_policy)
@override_settings(
DEBUG=True,
SECURE_CSP=basic_config,
SECURE_CSP_REPORT_ONLY=basic_config,
)
def test_csp_404_debug_view(self):
"""
Test that the CSP headers are not added to the debug view.
"""
response = self.client.get("/csp-404/")
self.assertNotIn(CSP.HEADER_ENFORCE, response)
self.assertNotIn(CSP.HEADER_REPORT_ONLY, response)
@override_settings(
DEBUG=True,
SECURE_CSP=basic_config,
SECURE_CSP_REPORT_ONLY=basic_config,
)
def test_csp_500_debug_view(self):
"""
Test that the CSP headers are not added to the debug view.
"""
response = self.client.get("/csp-500/")
self.assertNotIn(CSP.HEADER_ENFORCE, response)
self.assertNotIn(CSP.HEADER_REPORT_ONLY, response)
@override_settings(
ROOT_URLCONF="middleware.urls",
SECURE_CSP_REPORT_ONLY={
"default-src": [CSP.NONE],
"img-src": [CSP.SELF],
"script-src": [CSP.SELF],
"style-src": [CSP.SELF],
"report-uri": "/csp-report/",
},
)
@modify_settings(
MIDDLEWARE={"append": "django.middleware.csp.ContentSecurityPolicyMiddleware"}
)
class CSPSeleniumTestCase(SeleniumTestCase, StaticLiveServerTestCase):
available_apps = ["middleware"]
def setUp(self):
self.addCleanup(csp_reports.clear)
super().setUp()
def test_reports_are_generated(self):
url = self.live_server_url + "/csp-failure/"
self.selenium.get(url)
time.sleep(1) # Allow time for the CSP report to be sent.
reports = sorted(
(r["csp-report"]["document-uri"], r["csp-report"]["violated-directive"])
for r in csp_reports
)
self.assertEqual(reports, [(url, "img-src"), (url, "style-src-elem")])