#37184 closed Bug (fixed)
PBKDF2PasswordHasher enforces UTF-8 decodable requirement on passwords provided as bytes
| Reported by: | Johannes Leuschner | Owned by: | Jacob Walls |
|---|---|---|---|
| Component: | contrib.auth | Version: | 6.0 |
| Severity: | Release blocker | Keywords: | PBKDF2 hasher password bytes UTF-8 |
| Cc: | Johannes Leuschner, Sarah Boyce, Roelzkie | 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
Before 78fac1b0473ed8960ecd2a30aca4fa8420d150b8, PBKDF2PasswordHasher (which is the default hasher) used to accept a password of type bytes in make_password and check_password. After that commit, force_str is called on the password, raising a decoding error if the bytes are not valid UTF-8. The pbkdf2 implementation then actually converts back to bytes.
Minimal example:
from django.contrib.auth.hashers import make_password make_password(b"\xc0", hasher="pbkdf2_sha256") # fails with DjangoUnicodeDecodeError
and also
from django.contrib.auth.hashers import make_password, check_password encoded = make_password(b"", hasher="pbkdf2_sha256") check_password(b"\xc0", encoded) # fails with DjangoUnicodeDecodeError
A use-case for passing a password of type bytes is generated passwords/keys, which can be exposed to the user e.g. via base64 encoding. This maximizes password strength compared to only allowing valid UTF-8 characters, and generating random passwords with a restricted character set is not as straight-forward. Existing applications using passwords of type bytes now fail both at making and checking passwords.
Note that in the same commit force_str is also introduced to MD5PasswordHasher, but there it makes sense because .encode() has been called anyways, i.e. bytes was not supported before.
Change History (18)
comment:2 by , 3 weeks ago
| Owner: | set to |
|---|---|
| Status: | new → assigned |
comment:3 by , 3 weeks ago
| Cc: | added |
|---|---|
| Severity: | Normal → Release blocker |
| Summary: | PBKDF2PasswordHasher no longer accepts password of type bytes → PBKDF2PasswordHasher enforces UTF-8 decodable requirement on passwords provided as bytes |
| Triage Stage: | Unreviewed → Accepted |
Auth tests pass when I comment out the password = force_str(password) line (and would be hash-preserving, given what's pointed out above about the conversion back to bytes in the hasher). It looks like this was not discussed in review, so it appears unintentional. (It might have been added for parallelism with force_str(salt), but that's not a real parallel -- the salt gets string-interpolated, but the password doesn't.)
I agree we shouldn't impose a constraint on the universe of valid secure passwords at the hasher level. Forms and validators and management commands may expect strings only, but that's separate.
scrypt/argon2/bcrypt appear to accept these bytes, so we have a consistency issue (consistency was the rationale for accepting #36226).
MD5 is a different beast entirely because the password and salt get concatenated.
If we wanted some sort of canonicalization check, I don't think we'd put it in the hasher.
comment:4 by , 3 weeks ago
Thanks for taking ownership Vishy. I'm on vacation next week, so I'm hoping to have a PR approved by Friday of this week. Do you think you could turn this around quickly? No worries if not, I'll just assign to myself tomorrow.
comment:5 by , 3 weeks ago
I got this Jacob.
Seems more like a regression, force_str() looks redundant as the pbkdf2() makes a force_bytes() anyway!
comment:6 by , 3 weeks ago
| Has patch: | set |
|---|
comment:7 by , 3 weeks ago
I have added checks for each password hasher to maintain consistency for bytes passwords.
comment:9 by , 3 weeks ago
| Patch needs improvement: | set |
|---|
comment:10 by , 3 weeks ago
| Needs documentation: | unset |
|---|---|
| Owner: | changed from to |
| Patch needs improvement: | unset |
Pushed an example of what I mean here: PR
comment:11 by , 3 weeks ago
| Triage Stage: | Accepted → Ready for checkin |
|---|
The make_password() docs are a little confusing (or at least open to interpretation):
It looks like "plain-text … bytes" is really trying to say "utf-8 encoded text." If so, then
b"\xc0"is not a valid "plain-text bytes" password and the error is correct. And this should be treated as a docs clarification bug (for bothmake_password()andcheck_password()).But if "plain-text … bytes" actually means "a text password in the app's choice of encoding," then this is/was a breaking change. (And that's unclear to me: the password hashers have bounced back and forth between
force_bytes()andencode()over time: see #36226 and #27795. During theforce_bytes()periods, I think any encoding would have been acceptable so long as it was consistently applied.)