diff options
| author | Rob Hudson <rob@cogit8.org> | 2025-05-03 10:01:58 -0700 |
|---|---|---|
| committer | nessita <124304+nessita@users.noreply.github.com> | 2025-06-27 15:57:02 -0300 |
| commit | d63241ebc7067fdebbaf704989b34fcd8f26bbe9 (patch) | |
| tree | 07b5a5cb0c70c446f5f0fb9ad2834501fc3d6544 /tests/utils_tests | |
| parent | 3f59711581bd22ebd0f13fb040b15b69c0eee21f (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.py | 166 |
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) |
