VYPR
High severity7.1NVD Advisory· Published Dec 13, 2024· Updated Apr 15, 2026

CVE-2024-21543

CVE-2024-21543

Description

Versions of the package djoser before 2.3.0 are vulnerable to Authentication Bypass when the authenticate() function fails. This is because the system falls back to querying the database directly, granting access to users with valid credentials, and eventually bypassing custom authentication checks such as two-factor authentication, LDAP validations, or requirements from configured AUTHENTICATION_BACKENDS.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
djoserPyPI
< 2.3.02.3.0

Patches

2
d33c3993c0c7

rollback 8f65bfff16577c7fb0f52bbabf5fb69f6809ba62, add support for ModelBackend.user_can_authenticate

https://github.com/sunscrapers/djoserTom WojcikMay 3, 2024via ghsa
2 files changed · +220 24
  • djoser/serializers.py+3 7 modified
    @@ -118,17 +118,13 @@ def __init__(self, *args, **kwargs):
     
         def validate(self, attrs):
             password = attrs.get("password")
    -        params = {settings.LOGIN_FIELD: attrs.get(settings.LOGIN_FIELD)}
    +        params = {"username": attrs.get(settings.LOGIN_FIELD)}
             self.user = authenticate(
                 request=self.context.get("request"), **params, password=password
             )
             if not self.user:
    -            self.user = User.objects.filter(**params).first()
    -            if self.user and not self.user.check_password(password):
    -                self.fail("invalid_credentials")
    -        if self.user and self.user.is_active:
    -            return attrs
    -        self.fail("invalid_credentials")
    +            self.fail("invalid_credentials")
    +        return attrs
     
     
     class UserFunctionsMixin:
    
  • testproject/testapp/tests/test_token_create.py+217 17 modified
    @@ -1,6 +1,9 @@
     import django
    +from unittest import mock
    +
     from django.conf import settings as django_settings
     from django.contrib.auth import user_logged_in, user_login_failed
    +from django.contrib.auth.backends import ModelBackend
     from django.test import override_settings
     from djet import assertions
     from rest_framework import status
    @@ -37,9 +40,221 @@ def test_post_should_login_user(self):
             self.assertNotEqual(user.last_login, previous_last_login)
             self.assertTrue(self.signal_sent)
     
    +    @override_settings(
    +        AUTHENTICATION_BACKENDS=[
    +            "django.contrib.auth.backends.ModelBackend",
    +        ]
    +    )
    +    @override_settings(
    +        DJOSER=dict(django_settings.DJOSER, **{"LOGIN_FIELD": "username"})
    +    )
    +    def test_post_should_not_login_if_model_backend_user_can_authenticate__LOGIN_FIELD_username__USERNAME_FIELD_username(  # noqa: E501
    +        self,
    +    ):
    +        user = create_user()
    +        user_logged_in.connect(self.signal_receiver)
    +        previous_last_login = user.last_login
    +
    +        with mock.patch("djoser.serializers.User.USERNAME_FIELD", "username"):
    +            with mock.patch.object(
    +                ModelBackend, "user_can_authenticate", return_value=True
    +            ):
    +                response = self.client.post(
    +                    self.base_url,
    +                    {"username": user.username, "password": user.raw_password},
    +                )
    +                self.assert_status_equal(response, status.HTTP_200_OK)
    +
    +                user.refresh_from_db()
    +                self.assertEqual(response.data["auth_token"], user.auth_token.key)
    +                self.assertNotEqual(user.last_login, previous_last_login)
    +                self.assertTrue(self.signal_sent)
    +
    +            with mock.patch.object(
    +                ModelBackend, "user_can_authenticate", return_value=False
    +            ):
    +                response = self.client.post(
    +                    self.base_url,
    +                    {"username": user.username, "password": user.raw_password},
    +                )
    +                self.assert_status_equal(response, status.HTTP_400_BAD_REQUEST)
    +
    +            with mock.patch.object(
    +                ModelBackend, "user_can_authenticate", return_value=True
    +            ):
    +                response = self.client.post(
    +                    self.base_url, {"email": user.email, "password": user.raw_password}
    +                )
    +                self.assert_status_equal(response, status.HTTP_400_BAD_REQUEST)
    +
    +            with mock.patch.object(
    +                ModelBackend, "user_can_authenticate", return_value=False
    +            ):
    +                response = self.client.post(
    +                    self.base_url, {"email": user.email, "password": user.raw_password}
    +                )
    +                self.assert_status_equal(response, status.HTTP_400_BAD_REQUEST)
    +
    +    @override_settings(
    +        AUTHENTICATION_BACKENDS=[
    +            "django.contrib.auth.backends.ModelBackend",
    +        ]
    +    )
    +    @override_settings(DJOSER=dict(django_settings.DJOSER, **{"LOGIN_FIELD": "email"}))
    +    def test_post_should_not_login_if_model_backend_user_can_authenticate__LOGIN_FIELD_email__USERNAME_FIELD_username(  # noqa: E501
    +        self,
    +    ):
    +        user = create_user()
    +        user_logged_in.connect(self.signal_receiver)
    +        previous_last_login = user.last_login
    +
    +        with mock.patch("djoser.serializers.User.USERNAME_FIELD", "username"):
    +            with mock.patch.object(
    +                ModelBackend, "user_can_authenticate", return_value=True
    +            ):
    +                response = self.client.post(
    +                    self.base_url,
    +                    {"username": user.username, "password": user.raw_password},
    +                )
    +                self.assert_status_equal(response, status.HTTP_400_BAD_REQUEST)
    +
    +            with mock.patch.object(
    +                ModelBackend, "user_can_authenticate", return_value=False
    +            ):
    +                response = self.client.post(
    +                    self.base_url,
    +                    {"username": user.username, "password": user.raw_password},
    +                )
    +                self.assert_status_equal(response, status.HTTP_400_BAD_REQUEST)
    +
    +            with mock.patch.object(
    +                ModelBackend, "user_can_authenticate", return_value=True
    +            ):
    +                response = self.client.post(
    +                    self.base_url, {"email": user.email, "password": user.raw_password}
    +                )
    +                self.assert_status_equal(response, status.HTTP_400_BAD_REQUEST)
    +
    +            with mock.patch.object(
    +                ModelBackend, "user_can_authenticate", return_value=False
    +            ):
    +                response = self.client.post(
    +                    self.base_url, {"email": user.email, "password": user.raw_password}
    +                )
    +                self.assert_status_equal(response, status.HTTP_400_BAD_REQUEST)
    +
    +        user.refresh_from_db()
    +        self.assertEqual(user.last_login, previous_last_login)
    +        self.assertFalse(self.signal_sent)
    +
    +    @override_settings(
    +        AUTHENTICATION_BACKENDS=[
    +            "django.contrib.auth.backends.ModelBackend",
    +        ]
    +    )
    +    @override_settings(
    +        DJOSER=dict(django_settings.DJOSER, **{"LOGIN_FIELD": "username"})
    +    )
    +    def test_post_should_not_login_if_model_backend_user_can_authenticate__LOGIN_FIELD_username__USERNAME_FIELD_email(  # noqa: E501
    +        self,
    +    ):
    +        user = create_user()
    +        user_logged_in.connect(self.signal_receiver)
    +        previous_last_login = user.last_login
    +
    +        with mock.patch("djoser.serializers.User.USERNAME_FIELD", "email"):
    +            with mock.patch.object(
    +                ModelBackend, "user_can_authenticate", return_value=True
    +            ):
    +                response = self.client.post(
    +                    self.base_url,
    +                    {"username": user.username, "password": user.raw_password},
    +                )
    +                self.assert_status_equal(response, status.HTTP_400_BAD_REQUEST)
    +
    +            with mock.patch.object(
    +                ModelBackend, "user_can_authenticate", return_value=False
    +            ):
    +                response = self.client.post(
    +                    self.base_url,
    +                    {"username": user.username, "password": user.raw_password},
    +                )
    +                self.assert_status_equal(response, status.HTTP_400_BAD_REQUEST)
    +
    +            with mock.patch.object(
    +                ModelBackend, "user_can_authenticate", return_value=True
    +            ):
    +                response = self.client.post(
    +                    self.base_url, {"email": user.email, "password": user.raw_password}
    +                )
    +                self.assert_status_equal(response, status.HTTP_400_BAD_REQUEST)
    +
    +            with mock.patch.object(
    +                ModelBackend, "user_can_authenticate", return_value=False
    +            ):
    +                response = self.client.post(
    +                    self.base_url, {"email": user.email, "password": user.raw_password}
    +                )
    +                self.assert_status_equal(response, status.HTTP_400_BAD_REQUEST)
    +
    +        user.refresh_from_db()
    +        self.assertEqual(user.last_login, previous_last_login)
    +        self.assertFalse(self.signal_sent)
    +
    +    @override_settings(
    +        AUTHENTICATION_BACKENDS=[
    +            "django.contrib.auth.backends.ModelBackend",
    +        ]
    +    )
    +    @override_settings(DJOSER=dict(django_settings.DJOSER, **{"LOGIN_FIELD": "email"}))
    +    def test_post_should_not_login_if_model_backend_user_can_authenticate__LOGIN_FIELD_email__USERNAME_FIELD_email(  # noqa: E501
    +        self,
    +    ):
    +        user = create_user()
    +        user_logged_in.connect(self.signal_receiver)
    +        previous_last_login = user.last_login
    +
    +        with mock.patch("djoser.serializers.User.USERNAME_FIELD", "email"):
    +            with mock.patch.object(
    +                ModelBackend, "user_can_authenticate", return_value=True
    +            ):
    +                response = self.client.post(
    +                    self.base_url,
    +                    {"username": user.username, "password": user.raw_password},
    +                )
    +                self.assert_status_equal(response, status.HTTP_400_BAD_REQUEST)
    +
    +            with mock.patch.object(
    +                ModelBackend, "user_can_authenticate", return_value=False
    +            ):
    +                response = self.client.post(
    +                    self.base_url,
    +                    {"username": user.username, "password": user.raw_password},
    +                )
    +                self.assert_status_equal(response, status.HTTP_400_BAD_REQUEST)
    +
    +            with mock.patch.object(
    +                ModelBackend, "user_can_authenticate", return_value=True
    +            ):
    +                response = self.client.post(
    +                    self.base_url, {"email": user.email, "password": user.raw_password}
    +                )
    +                self.assert_status_equal(response, status.HTTP_200_OK)
    +
    +                user.refresh_from_db()
    +                self.assertEqual(response.data["auth_token"], user.auth_token.key)
    +                self.assertNotEqual(user.last_login, previous_last_login)
    +                self.assertTrue(self.signal_sent)
    +
    +            with mock.patch.object(
    +                ModelBackend, "user_can_authenticate", return_value=False
    +            ):
    +                response = self.client.post(
    +                    self.base_url, {"email": user.email, "password": user.raw_password}
    +                )
    +                self.assert_status_equal(response, status.HTTP_400_BAD_REQUEST)
    +
         def test_post_should_not_login_if_user_is_not_active(self):
    -        """In Django >= 1.10 authenticate() returns None if user is inactive,
    -        while in Django < 1.10 authenticate() succeeds if user is inactive."""
             user = create_user()
             data = {"username": user.username, "password": user.raw_password}
             user.is_active = False
    @@ -81,18 +296,3 @@ def test_post_should_not_login_if_empty_request(self):
                 response.data["non_field_errors"],
                 [settings.CONSTANTS.messages.INVALID_CREDENTIALS_ERROR],
             )
    -
    -    @override_settings(DJOSER=dict(django_settings.DJOSER, **{"LOGIN_FIELD": "email"}))
    -    def test_login_using_email(self):
    -        user = create_user()
    -        previous_last_login = user.last_login
    -        data = {"email": user.email, "password": user.raw_password}
    -        user_logged_in.connect(self.signal_receiver)
    -
    -        response = self.client.post(self.base_url, data)
    -        user.refresh_from_db()
    -
    -        self.assert_status_equal(response, status.HTTP_200_OK)
    -        self.assertEqual(response.data["auth_token"], user.auth_token.key)
    -        self.assertNotEqual(user.last_login, previous_last_login)
    -        self.assertTrue(self.signal_sent)
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

9

News mentions

0

No linked articles in our index yet.