diff options
| author | tschilling <schillingt@better-simple.com> | 2021-12-13 21:47:03 -0600 |
|---|---|---|
| committer | Mariusz Felisiak <felisiak.mariusz@gmail.com> | 2022-02-01 11:12:24 +0100 |
| commit | 0dcd549bbe36c060f536ec270d34d9e7d4b8e6c7 (patch) | |
| tree | 8b350cec1ab50d21cf6e3afd03f3db5f02fb1679 /tests | |
| parent | ba4a6880d1783190de4081bd456d934beb45cb19 (diff) | |
Fixed #30360 -- Added support for secret key rotation.
Thanks Florian Apolloner for the implementation idea.
Co-authored-by: Andreas Pelme <andreas@pelme.se>
Co-authored-by: Carlton Gibson <carlton.gibson@noumenal.es>
Co-authored-by: Vuyisile Ndlovu <terrameijar@gmail.com>
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/auth_tests/test_tokens.py | 36 | ||||
| -rw-r--r-- | tests/check_framework/test_security.py | 75 | ||||
| -rw-r--r-- | tests/settings_tests/tests.py | 1 | ||||
| -rw-r--r-- | tests/signing/tests.py | 35 | ||||
| -rw-r--r-- | tests/view_tests/tests/test_debug.py | 2 |
5 files changed, 147 insertions, 2 deletions
diff --git a/tests/auth_tests/test_tokens.py b/tests/auth_tests/test_tokens.py index ff26bd626b..2f3f52aac6 100644 --- a/tests/auth_tests/test_tokens.py +++ b/tests/auth_tests/test_tokens.py @@ -140,3 +140,39 @@ class TokenGeneratorTest(TestCase): msg = 'The SECRET_KEY setting must not be empty.' with self.assertRaisesMessage(ImproperlyConfigured, msg): default_token_generator.secret + + def test_check_token_secret_fallbacks(self): + user = User.objects.create_user('tokentestuser', 'test2@example.com', 'testpw') + p1 = PasswordResetTokenGenerator() + p1.secret = 'secret' + tk = p1.make_token(user) + p2 = PasswordResetTokenGenerator() + p2.secret = 'newsecret' + p2.secret_fallbacks = ['secret'] + self.assertIs(p1.check_token(user, tk), True) + self.assertIs(p2.check_token(user, tk), True) + + @override_settings( + SECRET_KEY='secret', + SECRET_KEY_FALLBACKS=['oldsecret'], + ) + def test_check_token_secret_key_fallbacks(self): + user = User.objects.create_user('tokentestuser', 'test2@example.com', 'testpw') + p1 = PasswordResetTokenGenerator() + p1.secret = 'oldsecret' + tk = p1.make_token(user) + p2 = PasswordResetTokenGenerator() + self.assertIs(p2.check_token(user, tk), True) + + @override_settings( + SECRET_KEY='secret', + SECRET_KEY_FALLBACKS=['oldsecret'], + ) + def test_check_token_secret_key_fallbacks_override(self): + user = User.objects.create_user('tokentestuser', 'test2@example.com', 'testpw') + p1 = PasswordResetTokenGenerator() + p1.secret = 'oldsecret' + tk = p1.make_token(user) + p2 = PasswordResetTokenGenerator() + p2.secret_fallbacks = [] + self.assertIs(p2.check_token(user, tk), False) diff --git a/tests/check_framework/test_security.py b/tests/check_framework/test_security.py index 774ba068f9..9002deefc5 100644 --- a/tests/check_framework/test_security.py +++ b/tests/check_framework/test_security.py @@ -1,5 +1,5 @@ from django.conf import settings -from django.core.checks.messages import Error +from django.core.checks.messages import Error, Warning from django.core.checks.security import base, csrf, sessions from django.core.management.utils import get_random_secret_key from django.test import SimpleTestCase @@ -414,6 +414,79 @@ class CheckSecretKeyTest(SimpleTestCase): self.assertEqual(base.check_secret_key(None), [base.W009]) +class CheckSecretKeyFallbacksTest(SimpleTestCase): + @override_settings(SECRET_KEY_FALLBACKS=[('abcdefghijklmnopqrstuvwx' * 2) + 'ab']) + def test_okay_secret_key_fallbacks(self): + self.assertEqual( + len(settings.SECRET_KEY_FALLBACKS[0]), + base.SECRET_KEY_MIN_LENGTH, + ) + self.assertGreater( + len(set(settings.SECRET_KEY_FALLBACKS[0])), + base.SECRET_KEY_MIN_UNIQUE_CHARACTERS, + ) + self.assertEqual(base.check_secret_key_fallbacks(None), []) + + def test_no_secret_key_fallbacks(self): + with self.settings(SECRET_KEY_FALLBACKS=None): + del settings.SECRET_KEY_FALLBACKS + self.assertEqual(base.check_secret_key_fallbacks(None), [ + Warning(base.W025.msg % 'SECRET_KEY_FALLBACKS', id=base.W025.id), + ]) + + @override_settings(SECRET_KEY_FALLBACKS=[ + base.SECRET_KEY_INSECURE_PREFIX + get_random_secret_key() + ]) + def test_insecure_secret_key_fallbacks(self): + self.assertEqual(base.check_secret_key_fallbacks(None), [ + Warning(base.W025.msg % 'SECRET_KEY_FALLBACKS[0]', id=base.W025.id), + ]) + + @override_settings(SECRET_KEY_FALLBACKS=[('abcdefghijklmnopqrstuvwx' * 2) + 'a']) + def test_low_length_secret_key_fallbacks(self): + self.assertEqual( + len(settings.SECRET_KEY_FALLBACKS[0]), + base.SECRET_KEY_MIN_LENGTH - 1, + ) + self.assertEqual(base.check_secret_key_fallbacks(None), [ + Warning(base.W025.msg % 'SECRET_KEY_FALLBACKS[0]', id=base.W025.id), + ]) + + @override_settings(SECRET_KEY_FALLBACKS=['abcd' * 20]) + def test_low_entropy_secret_key_fallbacks(self): + self.assertGreater( + len(settings.SECRET_KEY_FALLBACKS[0]), + base.SECRET_KEY_MIN_LENGTH, + ) + self.assertLess( + len(set(settings.SECRET_KEY_FALLBACKS[0])), + base.SECRET_KEY_MIN_UNIQUE_CHARACTERS, + ) + self.assertEqual(base.check_secret_key_fallbacks(None), [ + Warning(base.W025.msg % 'SECRET_KEY_FALLBACKS[0]', id=base.W025.id), + ]) + + @override_settings(SECRET_KEY_FALLBACKS=[ + ('abcdefghijklmnopqrstuvwx' * 2) + 'ab', + 'badkey', + ]) + def test_multiple_keys(self): + self.assertEqual(base.check_secret_key_fallbacks(None), [ + Warning(base.W025.msg % 'SECRET_KEY_FALLBACKS[1]', id=base.W025.id), + ]) + + @override_settings(SECRET_KEY_FALLBACKS=[ + ('abcdefghijklmnopqrstuvwx' * 2) + 'ab', + 'badkey1', + 'badkey2', + ]) + def test_multiple_bad_keys(self): + self.assertEqual(base.check_secret_key_fallbacks(None), [ + Warning(base.W025.msg % 'SECRET_KEY_FALLBACKS[1]', id=base.W025.id), + Warning(base.W025.msg % 'SECRET_KEY_FALLBACKS[2]', id=base.W025.id), + ]) + + class CheckDebugTest(SimpleTestCase): @override_settings(DEBUG=True) def test_debug_true(self): diff --git a/tests/settings_tests/tests.py b/tests/settings_tests/tests.py index e958a984fa..ffe096087a 100644 --- a/tests/settings_tests/tests.py +++ b/tests/settings_tests/tests.py @@ -483,6 +483,7 @@ class TestListSettings(SimpleTestCase): "INSTALLED_APPS", "TEMPLATE_DIRS", "LOCALE_PATHS", + "SECRET_KEY_FALLBACKS", ) def test_tuple_settings(self): diff --git a/tests/signing/tests.py b/tests/signing/tests.py index 1bd7277bc6..c375f2dbf2 100644 --- a/tests/signing/tests.py +++ b/tests/signing/tests.py @@ -1,7 +1,7 @@ import datetime from django.core import signing -from django.test import SimpleTestCase +from django.test import SimpleTestCase, override_settings from django.test.utils import freeze_time from django.utils.crypto import InvalidAlgorithm @@ -178,6 +178,39 @@ class TestSigner(SimpleTestCase): with self.assertRaisesMessage(ValueError, msg % sep): signing.Signer(sep=sep) + def test_verify_with_non_default_key(self): + old_signer = signing.Signer('secret') + new_signer = signing.Signer('newsecret', fallback_keys=['othersecret', 'secret']) + signed = old_signer.sign('abc') + self.assertEqual(new_signer.unsign(signed), 'abc') + + def test_sign_unsign_multiple_keys(self): + """The default key is a valid verification key.""" + signer = signing.Signer('secret', fallback_keys=['oldsecret']) + signed = signer.sign('abc') + self.assertEqual(signer.unsign(signed), 'abc') + + @override_settings( + SECRET_KEY='secret', + SECRET_KEY_FALLBACKS=['oldsecret'], + ) + def test_sign_unsign_ignore_secret_key_fallbacks(self): + old_signer = signing.Signer('oldsecret') + signed = old_signer.sign('abc') + signer = signing.Signer(fallback_keys=[]) + with self.assertRaises(signing.BadSignature): + signer.unsign(signed) + + @override_settings( + SECRET_KEY='secret', + SECRET_KEY_FALLBACKS=['oldsecret'], + ) + def test_default_keys_verification(self): + old_signer = signing.Signer('oldsecret') + signed = old_signer.sign('abc') + signer = signing.Signer() + self.assertEqual(signer.unsign(signed), 'abc') + class TestTimestampSigner(SimpleTestCase): diff --git a/tests/view_tests/tests/test_debug.py b/tests/view_tests/tests/test_debug.py index 8eda91ec35..84d675b79b 100644 --- a/tests/view_tests/tests/test_debug.py +++ b/tests/view_tests/tests/test_debug.py @@ -1459,6 +1459,7 @@ class ExceptionReporterFilterTests(ExceptionReportTestMixin, LoggingCaptureMixin """ sensitive_settings = [ 'SECRET_KEY', + 'SECRET_KEY_FALLBACKS', 'PASSWORD', 'API_KEY', 'AUTH_TOKEN', @@ -1475,6 +1476,7 @@ class ExceptionReporterFilterTests(ExceptionReportTestMixin, LoggingCaptureMixin """ sensitive_settings = [ 'SECRET_KEY', + 'SECRET_KEY_FALLBACKS', 'PASSWORD', 'API_KEY', 'AUTH_TOKEN', |
