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.
| Package | Affected versions | Patched versions |
|---|---|---|
djoserPyPI | < 2.3.0 | 2.3.0 |
Patches
2d33c3993c0c7rollback 8f65bfff16577c7fb0f52bbabf5fb69f6809ba62, add support for ModelBackend.user_can_authenticate
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)
db80fc721290Vulnerability 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- github.com/advisories/GHSA-v49p-m6gh-747cghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-21543ghsaADVISORY
- github.com/pypa/advisory-database/tree/main/vulns/djoser/PYSEC-2024-158.yamlghsaWEB
- github.com/sunscrapers/djoser/commit/d33c3993c0c735f23cbedc60fa59fce69354f19dnvdWEB
- github.com/sunscrapers/djoser/issues/795nvdWEB
- github.com/sunscrapers/djoser/pull/819nvdWEB
- github.com/sunscrapers/djoser/releases/tag/2.3.0nvdWEB
- lists.debian.org/debian-lts-announce/2025/02/msg00023.htmlnvdWEB
- security.snyk.io/vuln/SNYK-PYTHON-DJOSER-8366540nvdWEB
News mentions
0No linked articles in our index yet.