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
19 changes: 19 additions & 0 deletions django/contrib/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,3 +391,22 @@ async def aupdate_session_auth_hash(request, user):
await request.session.acycle_key()
if hasattr(user, "get_session_auth_hash") and await request.auser() == user:
await request.session.aset(HASH_SESSION_KEY, user.get_session_auth_hash())


def check_password_with_timing_attack_mitigation(user, password):
"""
Checks password against the user's hash if there is a user, otherwise runs
the default password hasher to prevent user enumeration attacks (#20760).
"""
if user is None:
get_user_model()().set_password(password)
else:
return user.check_password(password)


async def acheck_password_with_timing_attack_mitigation(user, password):
"""See check_user_with_timing_attack_mitigation."""
if user is None:
get_user_model()().set_password(password)
else:
return await user.acheck_password(password)
32 changes: 17 additions & 15 deletions django/contrib/auth/backends.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
from asgiref.sync import sync_to_async

from django.contrib.auth import get_user_model
from django.contrib.auth import (
acheck_password_with_timing_attack_mitigation,
check_password_with_timing_attack_mitigation,
get_user_model,
)
from django.contrib.auth.models import Permission
from django.db.models import Exists, OuterRef, Q
from django.views.decorators.debug import sensitive_variables
Expand Down Expand Up @@ -66,12 +70,12 @@ def authenticate(self, request, username=None, password=None, **kwargs):
try:
user = UserModel._default_manager.get_by_natural_key(username)
except UserModel.DoesNotExist:
# Run the default password hasher once to reduce the timing
# difference between an existing and a nonexistent user (#20760).
UserModel().set_password(password)
else:
if user.check_password(password) and self.user_can_authenticate(user):
return user
user = None

if check_password_with_timing_attack_mitigation(
user, password
) and self.user_can_authenticate(user):
return user

@sensitive_variables("password")
async def aauthenticate(self, request, username=None, password=None, **kwargs):
Expand All @@ -82,14 +86,12 @@ async def aauthenticate(self, request, username=None, password=None, **kwargs):
try:
user = await UserModel._default_manager.aget_by_natural_key(username)
except UserModel.DoesNotExist:
# Run the default password hasher once to reduce the timing
# difference between an existing and a nonexistent user (#20760).
UserModel().set_password(password)
else:
if await user.acheck_password(password) and self.user_can_authenticate(
user
):
return user
user = None

if await acheck_password_with_timing_attack_mitigation(
user, password
) and self.user_can_authenticate(user):
return user

def user_can_authenticate(self, user):
"""
Expand Down
12 changes: 2 additions & 10 deletions django/contrib/auth/handlers/modwsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ def _get_user(username):
"""
Return the UserModel instance for `username`.

If no matching user exists, or if the user is inactive, return None, in
which case the default password hasher is run to mitigate timing attacks.
If no matching user exists, or if the user is inactive, return None.
"""
try:
user = UserModel._default_manager.get_by_natural_key(username)
Expand All @@ -18,12 +17,6 @@ def _get_user(username):
else:
if not user.is_active:
user = None

if user is None:
# Run the default password hasher once to reduce the timing difference
# between existing/active and nonexistent/inactive users (#20760).
UserModel().set_password("")

return user


Expand All @@ -43,8 +36,7 @@ def check_password(environ, username, password):
db.reset_queries()
try:
user = _get_user(username)
if user:
return user.check_password(password)
return auth.check_password_with_timing_attack_mitigation(user, password)
finally:
db.close_old_connections()

Expand Down
Loading