Opened 6 years ago
Closed 6 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
TypeErrorin 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_passwordbehaviour (passing anything butbytesorstrtovalidate_passwordraises theTypeErrorwith defaultsettings.AUTH_PASSWORD_VALIDATORS).
Discussion:
https://groups.google.com/forum/#!topic/django-developers/1Ap0zDjFa4E
Change History (16)
comment:1 by , 6 years ago
| Description: | modified (diff) |
|---|
follow-up: 3 comment:2 by , 6 years ago
comment:3 by , 6 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_passwordas 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_bytesutility toto_bytesand add default keyword argumentforce=True. Then, hasher'sencodemethod would useto_bytesfunction withforce=Falseargument. In all otherforce_bytesoccurrences in the codebase, changing fromforce_bytestoto_bytesshould 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 , 6 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 , 6 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 , 6 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 , 6 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 , 6 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 , 6 years ago
| Owner: | changed from to |
|---|---|
| Status: | new → assigned |
comment:12 by , 6 years ago
| Patch needs improvement: | set |
|---|
comment:13 by , 6 years ago
| Patch needs improvement: | unset |
|---|
comment:14 by , 6 years ago
| Triage Stage: | Accepted → Ready for checkin |
|---|
From a bit of testing it seems
make_passwordalready raisesTypeErrorfor invalid types onmasterIt is hasher specific though so I guess we could turn
not instance(password, str)into aTypeErrorfrommake_passworditself instead of relying on implementation details of hashers.