summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorRob Hudson <rob@cogit8.org>2025-08-23 12:23:53 -0700
committernessita <124304+nessita@users.noreply.github.com>2025-08-28 17:23:48 -0300
commit550822bceea227b07445d1852c4376b663c09ea4 (patch)
treec7b9a032939c7611156b54767c10059d7d6e373c /tests
parent292b9e6fe8f23491680d9cc60f328562e2b1c823 (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 'tests')
-rw-r--r--tests/decorators/test_csp.py95
-rw-r--r--tests/middleware/test_csp.py69
-rw-r--r--tests/middleware/urls.py6
-rw-r--r--tests/middleware/views.py40
4 files changed, 210 insertions, 0 deletions
diff --git a/tests/decorators/test_csp.py b/tests/decorators/test_csp.py
new file mode 100644
index 0000000000..2def6b0f79
--- /dev/null
+++ b/tests/decorators/test_csp.py
@@ -0,0 +1,95 @@
+from itertools import product
+
+from asgiref.sync import iscoroutinefunction
+
+from django.http import HttpRequest, HttpResponse
+from django.test import SimpleTestCase
+from django.utils.csp import CSP
+from django.views.decorators.csp import csp_override, csp_report_only_override
+
+basic_config = {
+ "default-src": [CSP.SELF],
+}
+
+
+class CSPOverrideDecoratorTest(SimpleTestCase):
+ def test_wrapped_sync_function_is_not_coroutine_function(self):
+ def sync_view(request):
+ return HttpResponse()
+
+ wrapped_view = csp_override({})(sync_view)
+ self.assertIs(iscoroutinefunction(wrapped_view), False)
+
+ def test_wrapped_async_function_is_coroutine_function(self):
+ async def async_view(request):
+ return HttpResponse()
+
+ wrapped_view = csp_override({})(async_view)
+ self.assertIs(iscoroutinefunction(wrapped_view), True)
+
+ def test_decorator_requires_mapping(self):
+ for config, decorator in product(
+ [None, 0, False, [], [1, 2, 3], 42, {4, 5}],
+ (csp_override, csp_report_only_override),
+ ):
+ with (
+ self.subTest(config=config, decorator=decorator),
+ self.assertRaisesMessage(TypeError, "CSP config should be a mapping"),
+ ):
+ decorator(config)
+
+ def test_csp_override(self):
+ @csp_override(basic_config)
+ def sync_view(request):
+ return HttpResponse("OK")
+
+ response = sync_view(HttpRequest())
+ self.assertEqual(response._csp_config, basic_config)
+ self.assertIs(hasattr(response, "_csp_ro_config"), False)
+
+ async def test_csp_override_async_view(self):
+ @csp_override(basic_config)
+ async def async_view(request):
+ return HttpResponse("OK")
+
+ response = await async_view(HttpRequest())
+ self.assertEqual(response._csp_config, basic_config)
+ self.assertIs(hasattr(response, "_csp_ro_config"), False)
+
+ def test_csp_report_only_override(self):
+ @csp_report_only_override(basic_config)
+ def sync_view(request):
+ return HttpResponse("OK")
+
+ response = sync_view(HttpRequest())
+ self.assertEqual(response._csp_ro_config, basic_config)
+ self.assertIs(hasattr(response, "_csp_config"), False)
+
+ async def test_csp_report_only_override_async_view(self):
+ @csp_report_only_override(basic_config)
+ async def async_view(request):
+ return HttpResponse("OK")
+
+ response = await async_view(HttpRequest())
+ self.assertEqual(response._csp_ro_config, basic_config)
+ self.assertIs(hasattr(response, "_csp_config"), False)
+
+ def test_csp_override_both(self):
+ @csp_override(basic_config)
+ @csp_report_only_override(basic_config)
+ def sync_view(request):
+ return HttpResponse("OK")
+
+ response = sync_view(HttpRequest())
+ self.assertEqual(response._csp_config, basic_config)
+ self.assertEqual(response._csp_ro_config, basic_config)
+
+ async def test_csp_override_both_async_view(self):
+ @csp_override(basic_config)
+ @csp_report_only_override(basic_config)
+ async def async_view(request):
+ return HttpResponse("OK")
+
+ response = await async_view(HttpRequest())
+ self.assertEqual(response._csp_config, basic_config)
+ self.assertEqual(response._csp_ro_config, basic_config)
diff --git a/tests/middleware/test_csp.py b/tests/middleware/test_csp.py
index e7a2452240..3ab732fe2f 100644
--- a/tests/middleware/test_csp.py
+++ b/tests/middleware/test_csp.py
@@ -107,6 +107,75 @@ class CSPMiddlewareTest(SimpleTestCase):
@override_settings(
+ MIDDLEWARE=["django.middleware.csp.ContentSecurityPolicyMiddleware"],
+ ROOT_URLCONF="middleware.urls",
+ SECURE_CSP=basic_config,
+ SECURE_CSP_REPORT_ONLY=basic_config,
+)
+class CSPMiddlewareWithDecoratedViewsTest(SimpleTestCase):
+ def test_no_decorators(self):
+ response = self.client.get("/csp-base/")
+ self.assertEqual(response[CSP.HEADER_ENFORCE], basic_policy)
+ self.assertEqual(response[CSP.HEADER_REPORT_ONLY], basic_policy)
+
+ def test_csp_disabled_enforced(self):
+ """
+ `csp_override({})` only disables the enforced CSP header.
+ """
+ response = self.client.get("/csp-disabled-enforced/")
+ self.assertNotIn(CSP.HEADER_ENFORCE, response)
+ self.assertEqual(response[CSP.HEADER_REPORT_ONLY], basic_policy)
+
+ def test_csp_report_only_disabled(self):
+ """
+ `csp_report_only_override({})` only disables the report-only header.
+ """
+ response = self.client.get("/csp-disabled-report-only/")
+ self.assertNotIn(CSP.HEADER_REPORT_ONLY, response)
+ self.assertEqual(response[CSP.HEADER_ENFORCE], basic_policy)
+
+ def test_csp_disabled_both(self):
+ """
+ Using both CSP decorators with empty mappings will clear both headers.
+ """
+ response = self.client.get("/csp-disabled-both/")
+ self.assertNotIn(CSP.HEADER_ENFORCE, response)
+ self.assertNotIn(CSP.HEADER_REPORT_ONLY, response)
+
+ def test_csp_override_enforced(self):
+ """
+ `csp_override` only overrides the enforced header.
+ """
+ response = self.client.get("/csp-override-enforced/")
+ self.assertEqual(
+ response[CSP.HEADER_ENFORCE], "default-src 'self'; img-src 'self' data:"
+ )
+ self.assertEqual(response[CSP.HEADER_REPORT_ONLY], basic_policy)
+
+ def test_csp_report_only_override(self):
+ """
+ `csp_report_only_override` only overrides the report-only header.
+ """
+ response = self.client.get("/csp-override-report-only/")
+ self.assertEqual(
+ response[CSP.HEADER_REPORT_ONLY], "default-src 'self'; img-src 'self' data:"
+ )
+ self.assertEqual(response[CSP.HEADER_ENFORCE], basic_policy)
+
+ def test_csp_override_both_decorator(self):
+ """
+ Using both CSP decorators overrides both CSP Django settings.
+ """
+ response = self.client.get("/csp-override-both/")
+ self.assertEqual(
+ response[CSP.HEADER_ENFORCE], "default-src 'self'; img-src 'self' data:"
+ )
+ self.assertEqual(
+ response[CSP.HEADER_REPORT_ONLY], "default-src 'self'; img-src 'self' data:"
+ )
+
+
+@override_settings(
ROOT_URLCONF="middleware.urls",
SECURE_CSP_REPORT_ONLY={
"default-src": [CSP.NONE],
diff --git a/tests/middleware/urls.py b/tests/middleware/urls.py
index 37120c7a54..bbd68d2050 100644
--- a/tests/middleware/urls.py
+++ b/tests/middleware/urls.py
@@ -17,5 +17,11 @@ urlpatterns = [
path("csp-report/", views.csp_report_view),
path("csp-base/", views.empty_view),
path("csp-nonce/", views.csp_nonce),
+ path("csp-disabled-both/", views.csp_disabled_both),
+ path("csp-disabled-enforced/", views.csp_disabled_enforced),
+ path("csp-disabled-report-only/", views.csp_disabled_ro),
+ path("csp-override-both/", views.csp_override_both),
+ path("csp-override-enforced/", views.csp_override_enforced),
+ path("csp-override-report-only/", views.csp_override_report_only),
path("csp-500/", views.csp_500),
]
diff --git a/tests/middleware/views.py b/tests/middleware/views.py
index 6dc3ca24c7..716ddec5fd 100644
--- a/tests/middleware/views.py
+++ b/tests/middleware/views.py
@@ -3,9 +3,11 @@ import sys
from django.http import HttpResponse
from django.middleware.csp import get_nonce
+from django.utils.csp import CSP
from django.utils.decorators import method_decorator
from django.views.debug import technical_500_response
from django.views.decorators.common import no_append_slash
+from django.views.decorators.csp import csp_override, csp_report_only_override
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import View
@@ -29,6 +31,44 @@ def csp_nonce(request):
return HttpResponse(get_nonce(request))
+@csp_override({})
+def csp_disabled_enforced(request):
+ return HttpResponse()
+
+
+@csp_report_only_override({})
+def csp_disabled_ro(request):
+ return HttpResponse()
+
+
+@csp_override({})
+@csp_report_only_override({})
+def csp_disabled_both(request):
+ return HttpResponse()
+
+
+csp_policy_override = {
+ "default-src": [CSP.SELF],
+ "img-src": [CSP.SELF, "data:"],
+}
+
+
+@csp_override(csp_policy_override)
+def csp_override_enforced(request):
+ return HttpResponse()
+
+
+@csp_report_only_override(csp_policy_override)
+def csp_override_report_only(request):
+ return HttpResponse()
+
+
+@csp_override(csp_policy_override)
+@csp_report_only_override(csp_policy_override)
+def csp_override_both(request):
+ return HttpResponse()
+
+
def csp_500(request):
try:
raise Exception