summaryrefslogtreecommitdiff
path: root/tests/utils_tests
diff options
context:
space:
mode:
authorRob Hudson <rob@cogit8.org>2025-05-03 10:01:58 -0700
committernessita <124304+nessita@users.noreply.github.com>2025-06-27 15:57:02 -0300
commitd63241ebc7067fdebbaf704989b34fcd8f26bbe9 (patch)
tree07b5a5cb0c70c446f5f0fb9ad2834501fc3d6544 /tests/utils_tests
parent3f59711581bd22ebd0f13fb040b15b69c0eee21f (diff)
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>
Diffstat (limited to 'tests/utils_tests')
-rw-r--r--tests/utils_tests/test_csp.py166
1 files changed, 166 insertions, 0 deletions
diff --git a/tests/utils_tests/test_csp.py b/tests/utils_tests/test_csp.py
new file mode 100644
index 0000000000..96c66538d0
--- /dev/null
+++ b/tests/utils_tests/test_csp.py
@@ -0,0 +1,166 @@
+from secrets import token_urlsafe
+from unittest.mock import patch
+
+from django.test import SimpleTestCase
+from django.utils.csp import CSP, LazyNonce, build_policy
+from django.utils.functional import empty
+
+basic_config = {
+ "default-src": [CSP.SELF],
+}
+alt_config = {
+ "default-src": [CSP.SELF, CSP.UNSAFE_INLINE],
+}
+basic_policy = "default-src 'self'"
+
+
+class CSPConstantsTests(SimpleTestCase):
+ def test_constants(self):
+ self.assertEqual(CSP.NONE, "'none'")
+ self.assertEqual(CSP.REPORT_SAMPLE, "'report-sample'")
+ self.assertEqual(CSP.SELF, "'self'")
+ self.assertEqual(CSP.STRICT_DYNAMIC, "'strict-dynamic'")
+ self.assertEqual(CSP.UNSAFE_EVAL, "'unsafe-eval'")
+ self.assertEqual(CSP.UNSAFE_HASHES, "'unsafe-hashes'")
+ self.assertEqual(CSP.UNSAFE_INLINE, "'unsafe-inline'")
+ self.assertEqual(CSP.WASM_UNSAFE_EVAL, "'wasm-unsafe-eval'")
+ self.assertEqual(CSP.NONCE, "<CSP_NONCE_SENTINEL>")
+
+
+class CSPBuildPolicyTest(SimpleTestCase):
+
+ def assertPolicyEqual(self, a, b):
+ parts_a = sorted(a.split("; ")) if a is not None else None
+ parts_b = sorted(b.split("; ")) if b is not None else None
+ self.assertEqual(parts_a, parts_b, f"Policies not equal: {a!r} != {b!r}")
+
+ def test_config_empty(self):
+ self.assertPolicyEqual(build_policy({}), "")
+
+ def test_config_basic(self):
+ self.assertPolicyEqual(build_policy(basic_config), basic_policy)
+
+ def test_config_multiple_directives(self):
+ policy = {
+ "default-src": [CSP.SELF],
+ "script-src": [CSP.NONE],
+ }
+ self.assertPolicyEqual(
+ build_policy(policy), "default-src 'self'; script-src 'none'"
+ )
+
+ def test_config_value_as_string(self):
+ """
+ Test that a single value can be passed as a string.
+ """
+ policy = {"default-src": CSP.SELF}
+ self.assertPolicyEqual(build_policy(policy), "default-src 'self'")
+
+ def test_config_value_as_tuple(self):
+ """
+ Test that a tuple can be passed as a value.
+ """
+ policy = {"default-src": (CSP.SELF, "foo.com")}
+ self.assertPolicyEqual(build_policy(policy), "default-src 'self' foo.com")
+
+ def test_config_value_as_set(self):
+ """
+ Test that a set can be passed as a value.
+
+ Sets are often used in Django settings to ensure uniqueness, however, sets are
+ unordered. The middleware ensures consistency via sorting if a set is passed.
+ """
+ policy = {"default-src": {CSP.SELF, "foo.com", "bar.com"}}
+ self.assertPolicyEqual(
+ build_policy(policy), "default-src 'self' bar.com foo.com"
+ )
+
+ def test_config_value_none(self):
+ """
+ Test that `None` removes the directive from the policy.
+
+ Useful in cases where the CSP config is scripted in some way or
+ explicitly not wanting to set a directive.
+ """
+ policy = {"default-src": [CSP.SELF], "script-src": None}
+ self.assertPolicyEqual(build_policy(policy), basic_policy)
+
+ def test_config_value_boolean_true(self):
+ policy = {"default-src": [CSP.SELF], "block-all-mixed-content": True}
+ self.assertPolicyEqual(
+ build_policy(policy), "default-src 'self'; block-all-mixed-content"
+ )
+
+ def test_config_value_boolean_false(self):
+ policy = {"default-src": [CSP.SELF], "block-all-mixed-content": False}
+ self.assertPolicyEqual(build_policy(policy), basic_policy)
+
+ def test_config_value_multiple_boolean(self):
+ policy = {
+ "default-src": [CSP.SELF],
+ "block-all-mixed-content": True,
+ "upgrade-insecure-requests": True,
+ }
+ self.assertPolicyEqual(
+ build_policy(policy),
+ "default-src 'self'; block-all-mixed-content; upgrade-insecure-requests",
+ )
+
+ def test_config_with_nonce_arg(self):
+ """
+ Test when the `CSP.NONCE` is not in the defined policy, the nonce
+ argument has no effect.
+ """
+ self.assertPolicyEqual(build_policy(basic_config, nonce="abc123"), basic_policy)
+
+ def test_config_with_nonce(self):
+ policy = {"default-src": [CSP.SELF, CSP.NONCE]}
+ self.assertPolicyEqual(
+ build_policy(policy, nonce="abc123"),
+ "default-src 'self' 'nonce-abc123'",
+ )
+
+ def test_config_with_multiple_nonces(self):
+ policy = {
+ "default-src": [CSP.SELF, CSP.NONCE],
+ "script-src": [CSP.SELF, CSP.NONCE],
+ }
+ self.assertPolicyEqual(
+ build_policy(policy, nonce="abc123"),
+ "default-src 'self' 'nonce-abc123'; script-src 'self' 'nonce-abc123'",
+ )
+
+ def test_config_with_empty_directive(self):
+ policy = {"default-src": []}
+ self.assertPolicyEqual(build_policy(policy), "")
+
+
+class LazyNonceTests(SimpleTestCase):
+ def test_generates_on_usage(self):
+ generated_tokens = []
+ nonce = LazyNonce()
+ self.assertFalse(nonce)
+ self.assertIs(nonce._wrapped, empty)
+
+ def memento_token_urlsafe(size):
+ generated_tokens.append(result := token_urlsafe(size))
+ return result
+
+ with patch("django.utils.csp.secrets.token_urlsafe", memento_token_urlsafe):
+ # Force usage, similar to template rendering, to generate the nonce.
+ val = str(nonce)
+
+ self.assertTrue(nonce)
+ self.assertEqual(nonce, val)
+ self.assertIsInstance(nonce, str)
+ self.assertEqual(len(val), 22) # Based on secrets.token_urlsafe of 16 bytes.
+ self.assertEqual(generated_tokens, [nonce])
+ # Also test the wrapped value.
+ self.assertEqual(nonce._wrapped, val)
+
+ def test_returns_same_value(self):
+ nonce = LazyNonce()
+ first = str(nonce)
+ second = str(nonce)
+
+ self.assertEqual(first, second)