Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions django/contrib/auth/hashers.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,6 @@ class PBKDF2PasswordHasher(BasePasswordHasher):
def encode(self, password, salt, iterations=None):
self._check_encode_args(password, salt)
iterations = iterations or self.iterations
password = force_str(password)
salt = force_str(salt)
hash = pbkdf2(password, salt, iterations, digest=self.digest)
hash = base64.b64encode(hash).decode("ascii").strip()
Expand Down Expand Up @@ -664,10 +663,8 @@ class MD5PasswordHasher(BasePasswordHasher):

def encode(self, password, salt):
self._check_encode_args(password, salt)
password = force_str(password)
salt = force_str(salt)
hash = hashlib.md5((salt + password).encode()).hexdigest()
return "%s$%s$%s" % (self.algorithm, salt, hash)
hash = hashlib.md5(force_bytes(salt) + force_bytes(password)).hexdigest()
return "%s$%s$%s" % (self.algorithm, force_str(salt), hash)

def decode(self, encoded):
algorithm, salt, hash = encoded.split("$", 2)
Expand Down
4 changes: 3 additions & 1 deletion django/core/cache/backends/filebased.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ def touch(self, key, timeout=DEFAULT_TIMEOUT, version=None):
self._write_content(f, timeout, previous_value)
return True
finally:
locks.unlock(f)
# Expired files are closed & unlocked by _is_expired().
if not f.closed:
locks.unlock(f)
except FileNotFoundError:
return False

Expand Down
5 changes: 4 additions & 1 deletion docs/releases/6.0.7.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,7 @@ Django 6.0.7 fixes several bugs in 6.0.6.
Bugfixes
========

* ...
* Fixed a regression in Django 6.0 where the PBKDF2 and MD5 password hashers
raised :exc:`UnicodeDecodeError` for :class:`bytes` passwords that were not
valid UTF-8. Passwords supplied as :class:`str` or as UTF-8 :class:`bytes`
are unaffected (:ticket:`37184`).
58 changes: 33 additions & 25 deletions tests/auth_tests/test_hashers.py
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,30 @@ def test_encode_invalid_salt(self):
with self.assertRaisesMessage(ValueError, msg):
hasher.encode("password", salt)

def assertHasherVerifiesStrAndBytes(self, hasher, salts):
"""
If a string representation exists for a bytes password, then both
representations will verify against whichever form was stored.
Otherwise, only the bytes representation will verify.
"""
for pass_str, pass_bytes in [
("password", b"password"),
# Valid UTF-8, but not ASCII.
("Lösenord", "Lösenord".encode()),
# Not UTF-8.
("", b"non-utf-8-\xc0"),
]:
with self.subTest(hasher=hasher.__class__.__name__, password=pass_bytes):
for password in (pass_str, pass_bytes):
if not password:
continue
for salt in salts:
encoded = hasher.encode(password, salt)
self.assertIs(hasher.verify(pass_bytes, encoded), True)
if not pass_str:
continue
self.assertIs(hasher.verify(pass_str, encoded), True)

def test_password_and_salt_in_str_and_bytes(self):
hasher_classes = [
MD5PasswordHasher,
Expand All @@ -531,25 +555,16 @@ def test_password_and_salt_in_str_and_bytes(self):
]
for hasher_class in hasher_classes:
hasher = hasher_class()
with self.subTest(hasher_class.__name__):
passwords = ["password", b"password"]
for password in passwords:
for salt in [hasher.salt(), hasher.salt().encode()]:
encoded = hasher.encode(password, salt)
for password_to_verify in passwords:
self.assertIs(
hasher.verify(password_to_verify, encoded), True
)
self.assertHasherVerifiesStrAndBytes(
hasher, [hasher.salt(), hasher.salt().encode()]
)

@skipUnless(argon2, "argon2-cffi not installed")
def test_password_and_salt_in_str_and_bytes_argon2(self):
hasher = Argon2PasswordHasher()
passwords = ["password", b"password"]
for password in passwords:
for salt in [hasher.salt(), hasher.salt().encode()]:
encoded = hasher.encode(password, salt)
for password_to_verify in passwords:
self.assertIs(hasher.verify(password_to_verify, encoded), True)
self.assertHasherVerifiesStrAndBytes(
hasher, [hasher.salt(), hasher.salt().encode()]
)

@skipUnless(bcrypt, "bcrypt not installed")
def test_password_and_salt_in_str_and_bytes_bcrypt(self):
Expand All @@ -559,16 +574,9 @@ def test_password_and_salt_in_str_and_bytes_bcrypt(self):
]
for hasher_class in hasher_classes:
hasher = hasher_class()
with self.subTest(hasher_class.__name__):
passwords = ["password", b"password"]
for password in passwords:
salts = [hasher.salt().decode(), hasher.salt()]
for salt in salts:
encoded = hasher.encode(password, salt)
for password_to_verify in passwords:
self.assertIs(
hasher.verify(password_to_verify, encoded), True
)
self.assertHasherVerifiesStrAndBytes(
hasher, [hasher.salt().decode(), hasher.salt()]
)

def test_encode_password_required(self):
hasher_classes = [
Expand Down
6 changes: 6 additions & 0 deletions tests/cache/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1971,6 +1971,12 @@ def sleep(self, seconds):
):
super().test_touch()

def test_touch_expired_key_does_not_crash(self):
cache.set("expired_touch_key", "value", timeout=0.01)
time.sleep(0.05)
result = cache.touch("expired_touch_key", 60)
self.assertIs(result, False)


@unittest.skipUnless(RedisCache_params, "Redis backend not configured")
@override_settings(
Expand Down
Loading