From e1606d27b4fed653c80817f3a13cf8bc6f3163f0 Mon Sep 17 00:00:00 2001 From: Natalia <124304+nessita@users.noreply.github.com> Date: Thu, 8 Aug 2024 11:29:00 -0300 Subject: [PATCH] Added test for acheck_password() to ensure make_password is called for unusable passwords. This is a follow up for the fix of CVE-2024-39329 (5d8645857936c142a3973694799c52165e2bdcdb) where the timing of verify_password() was standardized when checking unusable passwords. --- tests/auth_tests/test_hashers.py | 61 +++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/tests/auth_tests/test_hashers.py b/tests/auth_tests/test_hashers.py index 1b41c75e69..77242bca4f 100644 --- a/tests/auth_tests/test_hashers.py +++ b/tests/auth_tests/test_hashers.py @@ -1,3 +1,4 @@ +from contextlib import contextmanager from unittest import mock, skipUnless from django.conf.global_settings import PASSWORD_HASHERS @@ -452,8 +453,33 @@ class TestUtilsHashPass(SimpleTestCase): check_password("wrong_password", encoded) self.assertEqual(hasher.harden_runtime.call_count, 1) - def test_check_password_calls_make_password_to_fake_runtime(self): + @contextmanager + def assertMakePasswordCalled(self, password, encoded, hasher_side_effect): hasher = get_hasher("default") + with ( + mock.patch( + "django.contrib.auth.hashers.identify_hasher", + side_effect=hasher_side_effect, + ) as mock_identify_hasher, + mock.patch( + "django.contrib.auth.hashers.make_password" + ) as mock_make_password, + mock.patch( + "django.contrib.auth.hashers.get_random_string", + side_effect=lambda size: "x" * size, + ), + mock.patch.object(hasher, "verify"), + ): + # Ensure make_password is called to standardize timing. + yield + self.assertEqual(hasher.verify.call_count, 0) + self.assertEqual(mock_identify_hasher.mock_calls, [mock.call(encoded)]) + self.assertEqual( + mock_make_password.mock_calls, + [mock.call("x" * UNUSABLE_PASSWORD_SUFFIX_LENGTH)], + ) + + def test_check_password_calls_make_password_to_fake_runtime(self): cases = [ (None, None, None), # no plain text password provided ("foo", make_password(password=None), None), # unusable encoded @@ -462,27 +488,22 @@ class TestUtilsHashPass(SimpleTestCase): for password, encoded, hasher_side_effect in cases: with ( self.subTest(encoded=encoded), - mock.patch( - "django.contrib.auth.hashers.identify_hasher", - side_effect=hasher_side_effect, - ) as mock_identify_hasher, - mock.patch( - "django.contrib.auth.hashers.make_password" - ) as mock_make_password, - mock.patch( - "django.contrib.auth.hashers.get_random_string", - side_effect=lambda size: "x" * size, - ), - mock.patch.object(hasher, "verify"), + self.assertMakePasswordCalled(password, encoded, hasher_side_effect), ): - # Ensure make_password is called to standardize timing. check_password(password, encoded) - self.assertEqual(hasher.verify.call_count, 0) - self.assertEqual(mock_identify_hasher.mock_calls, [mock.call(encoded)]) - self.assertEqual( - mock_make_password.mock_calls, - [mock.call("x" * UNUSABLE_PASSWORD_SUFFIX_LENGTH)], - ) + + async def test_acheck_password_calls_make_password_to_fake_runtime(self): + cases = [ + (None, None, None), # no plain text password provided + ("foo", make_password(password=None), None), # unusable encoded + ("letmein", make_password(password="letmein"), ValueError), # valid encoded + ] + for password, encoded, hasher_side_effect in cases: + with ( + self.subTest(encoded=encoded), + self.assertMakePasswordCalled(password, encoded, hasher_side_effect), + ): + await acheck_password(password, encoded) def test_encode_invalid_salt(self): hasher_classes = [