Opened 5 years ago
Closed 5 years ago
#31375 closed Bug (fixed)
make_password shouldn't accept values other than bytes or string as an argument
Reported by: | iamdavidcz | Owned by: | Hasan Ramezani |
---|---|---|---|
Component: | contrib.auth | Version: | 3.0 |
Severity: | Normal | Keywords: | |
Cc: | Triage Stage: | Ready for checkin | |
Has patch: | yes | Needs documentation: | no |
Needs tests: | no | Patch needs improvement: | no |
Easy pickings: | no | UI/UX: | no |
Description (last modified by )
Currently make_password
function accepts almost every Python object as an argument. This is a strange behaviour and it results directly from force_bytes
casting objects to str
. We should throw the TypeError
when passing anything but bytes
or str
to make_password
.
Reasons:
- programmers unaware of this strange behaviour can accidentally create weak passwords (potential security issue)
- other libraries raise the
TypeError
in the same cases (eg. Werkzeug, passlib) - it's inconsistent with the documentation that says:
It takes one mandatory argument: the password in plain-text.
- it's inconsistent with
validate_password
behaviour (passing anything butbytes
orstr
tovalidate_password
raises theTypeError
with defaultsettings.AUTH_PASSWORD_VALIDATORS
).
Discussion:
https://groups.google.com/forum/#!topic/django-developers/1Ap0zDjFa4E
Change History (16)
comment:1 by , 5 years ago
Description: | modified (diff) |
---|
follow-up: 3 comment:2 by , 5 years ago
comment:3 by , 5 years ago
Simon Charette, thank you for your testing. Could you tell me what is your PASSWORD_HASHERS
setting? I've tried to reproduce your example on master using PBKDF2PasswordHasher
and this hasher does not raise the TypeError
on my side:
In [1]: class Object: ...: def __str__(self): ...: return 'foo' In [2]: from django.contrib.auth.hashers import get_hasher In [3]: hasher = get_hasher('default') In [4]: hasher Out[4]: <django.contrib.auth.hashers.PBKDF2PasswordHasher at 0x10cef7850> In [5]: salt = hasher.salt() In [6]: salt Out[6]: 'l9QFlyCku6VE' In [7]: hasher.encode(Object(), salt) Out[7]: 'pbkdf2_sha256$216000$l9QFlyCku6VE$qqMksofk6MSGevhG/I4xJ7AIRf+Hhq/7myi3pd6vSBU=' In [8]: hasher.encode('foo', salt) Out[8]: 'pbkdf2_sha256$216000$l9QFlyCku6VE$qqMksofk6MSGevhG/I4xJ7AIRf+Hhq/7myi3pd6vSBU='
Now I can see two options in order to make this type guard hasher agnostic:
- Add if statement to
make_password
as you suggested. Something like:if not isinstance(password, (bytes, str)): raise TypeError('password must be bytes or string (got %s).' % type(password).__name__)
- Change
force_bytes
utility toto_bytes
and add default keyword argumentforce=True
. Then, hasher'sencode
method would useto_bytes
function withforce=False
argument. In all otherforce_bytes
occurrences in the codebase, changing fromforce_bytes
toto_bytes
should be sufficient.
Edit: of course, we need to add a separate if statement for str
itself.
def to_bytes(s, encoding='utf-8', strings_only=False, errors='strict', force=True): if isinstance(s, bytes): if encoding == 'utf-8': return s else: return s.decode('utf-8', errors).encode(encoding, errors) if strings_only and is_protected_type(s): return s if isinstance(s, memoryview): return bytes(s) if isinstance(s, str): return s.encode(encoding, errors) if force: return str(s).encode(encoding, errors) raise TypeError('cannot convert %s object to bytes' % type(s).__name__)
However, this solution would require much more effort and maintaining backward compatibility. The first option seems to be easier.
comment:4 by , 5 years ago
I was using MD5PasswordHasher
which is the default for test settings; PBKDF2PasswordHasher.encode
seems to be accepting whatever value is provided.
follow-up: 6 comment:5 by , 5 years ago
I think force_bytes
is only used by PBKDF2PasswordHasher
and PBKDF2SHA1PasswordHasher
through django.utils.crypto.pbkdf2
. Some Django's included hashers depend on hashlib
library while others depend on third party libraries using _load_library()
method for password encoding. Examples:
As Simon Charette said, it is hasher specific. So if we want to make it language agnostic, I think the first option will do (adding if not isinstance(password, (bytes, str))
in make_password
)
comment:6 by , 5 years ago
Replying to hashlash:
As Simon Charette said, it is hasher specific. So if we want to make it language agnostic, I think the first option will do (adding
if not isinstance(password, (bytes, str))
inmake_password
)
Fully agree. Let's choose the first option then.
comment:7 by , 5 years ago
Simon , if you agree with the proposed solution :
if not isinstance(password, (bytes, str)): raise TypeError('password must be bytes or string (got %s).' % type(password).__name__)
I can prepare a patch for it.
comment:9 by , 5 years ago
Triage Stage: | Unreviewed → Accepted |
---|
OK, I think this is pretty marginal, but let's have it.
A patch needs to include a release note, saying approximately, "make_password()
now requires its argument to be a string or bytes. Other types should be explicitly cast to one of these, if required".
comment:10 by , 5 years ago
Owner: | changed from | to
---|---|
Status: | new → assigned |
comment:12 by , 5 years ago
Patch needs improvement: | set |
---|
comment:13 by , 5 years ago
Patch needs improvement: | unset |
---|
comment:14 by , 5 years ago
Triage Stage: | Accepted → Ready for checkin |
---|
From a bit of testing it seems
make_password
already raisesTypeError
for invalid types onmaster
It is hasher specific though so I guess we could turn
not instance(password, str)
into aTypeError
frommake_password
itself instead of relying on implementation details of hashers.