1
0
mirror of https://github.com/django/django.git synced 2025-08-21 09:19:12 +00:00

Fixed #36226 -- Accepted str or bytes for password and salt in password hashers.

Co-authored-by: Screamadelica <1621456391@sjtu.edu.cn>
This commit is contained in:
Roel Delos Reyes 2025-07-09 00:14:00 +08:00 committed by Sarah Boyce
parent e709301000
commit 78fac1b047
3 changed files with 63 additions and 7 deletions

View File

@ -903,6 +903,7 @@ answer newbie questions, and generally made Django that much better:
Rob Nguyen <tienrobertnguyenn@gmail.com> Rob Nguyen <tienrobertnguyenn@gmail.com>
Robin Munn <http://www.geekforgod.com/> Robin Munn <http://www.geekforgod.com/>
Rodrigo Pinheiro Marques de Araújo <fenrrir@gmail.com> Rodrigo Pinheiro Marques de Araújo <fenrrir@gmail.com>
Roel Delos Reyes <https://roelzkie.dev>
Rohith P R <https://rohithpr.com> Rohith P R <https://rohithpr.com>
Romain Garrigues <romain.garrigues.cs@gmail.com> Romain Garrigues <romain.garrigues.cs@gmail.com>
Ronnie van den Crommenacker Ronnie van den Crommenacker

View File

@ -16,6 +16,7 @@ from django.utils.crypto import (
get_random_string, get_random_string,
pbkdf2, pbkdf2,
) )
from django.utils.encoding import force_bytes, force_str
from django.utils.module_loading import import_string from django.utils.module_loading import import_string
from django.utils.translation import gettext_noop as _ from django.utils.translation import gettext_noop as _
@ -252,7 +253,7 @@ class BasePasswordHasher:
def _check_encode_args(self, password, salt): def _check_encode_args(self, password, salt):
if password is None: if password is None:
raise TypeError("password must be provided.") raise TypeError("password must be provided.")
if not salt or "$" in salt: if not salt or "$" in force_str(salt): # salt can be str or bytes.
raise ValueError("salt must be provided and cannot contain $.") raise ValueError("salt must be provided and cannot contain $.")
def encode(self, password, salt): def encode(self, password, salt):
@ -324,6 +325,8 @@ class PBKDF2PasswordHasher(BasePasswordHasher):
def encode(self, password, salt, iterations=None): def encode(self, password, salt, iterations=None):
self._check_encode_args(password, salt) self._check_encode_args(password, salt)
iterations = iterations or self.iterations iterations = iterations or self.iterations
password = force_str(password)
salt = force_str(salt)
hash = pbkdf2(password, salt, iterations, digest=self.digest) hash = pbkdf2(password, salt, iterations, digest=self.digest)
hash = base64.b64encode(hash).decode("ascii").strip() hash = base64.b64encode(hash).decode("ascii").strip()
return "%s$%d$%s$%s" % (self.algorithm, iterations, salt, hash) return "%s$%d$%s$%s" % (self.algorithm, iterations, salt, hash)
@ -396,8 +399,8 @@ class Argon2PasswordHasher(BasePasswordHasher):
argon2 = self._load_library() argon2 = self._load_library()
params = self.params() params = self.params()
data = argon2.low_level.hash_secret( data = argon2.low_level.hash_secret(
password.encode(), force_bytes(password),
salt.encode(), force_bytes(salt),
time_cost=params.time_cost, time_cost=params.time_cost,
memory_cost=params.memory_cost, memory_cost=params.memory_cost,
parallelism=params.parallelism, parallelism=params.parallelism,
@ -499,7 +502,8 @@ class BCryptSHA256PasswordHasher(BasePasswordHasher):
def encode(self, password, salt): def encode(self, password, salt):
bcrypt = self._load_library() bcrypt = self._load_library()
password = password.encode() password = force_bytes(password)
salt = force_bytes(salt)
# Hash the password prior to using bcrypt to prevent password # Hash the password prior to using bcrypt to prevent password
# truncation as described in #20138. # truncation as described in #20138.
if self.digest is not None: if self.digest is not None:
@ -585,8 +589,8 @@ class ScryptPasswordHasher(BasePasswordHasher):
r = r or self.block_size r = r or self.block_size
p = p or self.parallelism p = p or self.parallelism
hash_ = hashlib.scrypt( hash_ = hashlib.scrypt(
password.encode(), password=force_bytes(password),
salt=salt.encode(), salt=force_bytes(salt),
n=n, n=n,
r=r, r=r,
p=p, p=p,
@ -594,7 +598,7 @@ class ScryptPasswordHasher(BasePasswordHasher):
dklen=64, dklen=64,
) )
hash_ = base64.b64encode(hash_).decode("ascii").strip() hash_ = base64.b64encode(hash_).decode("ascii").strip()
return "%s$%d$%s$%d$%d$%s" % (self.algorithm, n, salt, r, p, hash_) return "%s$%d$%s$%d$%d$%s" % (self.algorithm, n, force_str(salt), r, p, hash_)
def decode(self, encoded): def decode(self, encoded):
algorithm, work_factor, salt, block_size, parallelism, hash_ = encoded.split( algorithm, work_factor, salt, block_size, parallelism, hash_ = encoded.split(
@ -655,6 +659,8 @@ class MD5PasswordHasher(BasePasswordHasher):
def encode(self, password, salt): def encode(self, password, salt):
self._check_encode_args(password, salt) self._check_encode_args(password, salt)
password = force_str(password)
salt = force_str(salt)
hash = hashlib.md5((salt + password).encode()).hexdigest() hash = hashlib.md5((salt + password).encode()).hexdigest()
return "%s$%s$%s" % (self.algorithm, salt, hash) return "%s$%s$%s" % (self.algorithm, salt, hash)

View File

@ -5,6 +5,7 @@ from django.conf.global_settings import PASSWORD_HASHERS
from django.contrib.auth.hashers import ( from django.contrib.auth.hashers import (
UNUSABLE_PASSWORD_PREFIX, UNUSABLE_PASSWORD_PREFIX,
UNUSABLE_PASSWORD_SUFFIX_LENGTH, UNUSABLE_PASSWORD_SUFFIX_LENGTH,
Argon2PasswordHasher,
BasePasswordHasher, BasePasswordHasher,
BCryptPasswordHasher, BCryptPasswordHasher,
BCryptSHA256PasswordHasher, BCryptSHA256PasswordHasher,
@ -520,6 +521,54 @@ class TestUtilsHashPass(SimpleTestCase):
with self.assertRaisesMessage(ValueError, msg): with self.assertRaisesMessage(ValueError, msg):
hasher.encode("password", salt) hasher.encode("password", salt)
def test_password_and_salt_in_str_and_bytes(self):
hasher_classes = [
MD5PasswordHasher,
PBKDF2PasswordHasher,
PBKDF2SHA1PasswordHasher,
ScryptPasswordHasher,
]
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
)
@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)
@skipUnless(bcrypt, "bcrypt not installed")
def test_password_and_salt_in_str_and_bytes_bcrypt(self):
hasher_classes = [
BCryptPasswordHasher,
BCryptSHA256PasswordHasher,
]
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
)
def test_encode_password_required(self): def test_encode_password_required(self):
hasher_classes = [ hasher_classes = [
MD5PasswordHasher, MD5PasswordHasher,