CVE-2022-24857: Improper Authentication (CWE-287)
value: django-mfa3 is a library that implements multi factor authentication for the django web framework. It achieves this by modifying the regular login view. Django however has a second login view for its admin area. This second login view was not modified, so the multi factor authentication can be bypassed. Users are affected if they have activated both django-mfa3 (< 0.5.0) and django.contrib.admin and have not taken any other measures to prevent users from accessing the admin login view. The issue has been fixed in django-mfa3 0.5.0. It is possible to work around the issue by overwriting the admin login route, e.g. by adding the following URL definition *before* the admin routes: url('admin/login/', lambda request: redirect(settings.LOGIN_URL)
CWE description: When an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct.
CVSS v3 base score: 8.8
Published: 2022-04-15T19:15Z
Repository: https://github.com/xi/django-mfa3
Language: Python
Files changed: README.md, mfa/admin.py, mfa/apps.py, tests/tests.py, tests/urls.py
Commit date: 2022-04-15 12:45:28 +0200
Commit message: fix bypass via admin login

inspired by django-two-factor-auth

Diff: diff --git a/README.md b/README.md
index dcf7be5..8651568 100644
--- a/README.md
+++ b/README.md
@@ -19,11 +19,14 @@ pip install django-mfa3
 ```
 
 ## Usage
 
 1.  Add `'mfa'` to `INSTALLED_APPS`
-2.  Use `mfa.views.LoginView` instead of the regular login view
+2.  Use `mfa.views.LoginView` instead of the regular login view. (Be sure to
+    remove any other login routes, otherwise the multi factor authentication
+    can be circumvented. The admin login will automatically be patched to
+    redirect to the regular login.)
 3.  Set `MFA_DOMAIN = 'example.com'` and `MFA_SITE_TITLE = 'My site'`
 4.  Register URLs: `path('mfa/', include('mfa.urls', namespace='mfa')`
 5.  The included templates are just examples, so you should [replace them](https://docs.djangoproject.com/en/stable/howto/overriding-templates/) with your own
 6.  Somewhere in your app, add a link to `'mfa:list'`
 
diff --git a/mfa/admin.py b/mfa/admin.py
index 747904c..2d9c650 100644
--- a/mfa/admin.py
+++ b/mfa/admin.py
@@ -1,8 +1,30 @@
 from django.contrib import admin
+from django.contrib.auth import REDIRECT_FIELD_NAME
+from django.contrib.auth.views import redirect_to_login
+from django.urls import reverse
 
 from .models import MFAKey
 
+original_login = admin.AdminSite.login
+
+
+def custom_login(self, request, extra_context=None):
+    next_url = (
+        request.GET.get(REDIRECT_FIELD_NAME)
+        or request.POST.get(REDIRECT_FIELD_NAME)
+        or reverse('admin:index')
+    )
+    return redirect_to_login(next_url)
+
+
+def patch_admin():
+    setattr(admin.AdminSite, 'login', custom_login)
+
+
+def unpatch_admin():
+    setattr(admin.AdminSite, 'login', original_login)
+
 
 @admin.register(MFAKey)
 class MFAKeyAdmin(admin.ModelAdmin):
     list_display = ['name', 'user', 'method']
diff --git a/mfa/apps.py b/mfa/apps.py
new file mode 100644
index 0000000..ea7b0b4
--- /dev/null
+++ b/mfa/apps.py
@@ -0,0 +1,10 @@
+from django.apps import AppConfig
+
+
+class TwoFactorConfig(AppConfig):
+    name = 'mfa'
+    verbose_name = 'Multi Factor Authentication'
+
+    def ready(self):
+        from .admin import patch_admin
+        patch_admin()
diff --git a/tests/tests.py b/tests/tests.py
index 59a7991..91f30c5 100644
--- a/tests/tests.py
+++ b/tests/tests.py
@@ -260,10 +260,35 @@ class MFAEnforceMiddlewareTest(MFATestCase):
         res = self.client.get('/logout/')
         self.assertEqual(res.status_code, 302)
         self.assertEqual(res.url, '/')
 
 
+class PatchAdminTest(TestCase):
+    def test_root(self):
+        res = self.client.get('/admin/')
+        self.assertEqual(res.status_code, 302)
+        self.assertEqual(res.url, '/admin/login/?next=/admin/')
+
+        res = self.client.get(res.url)
+        self.assertEqual(res.status_code, 302)
+        self.assertEqual(res.url, '/login/?next=/admin/')
+
+    def test_app(self):
+        res = self.client.get('/admin/mfa/')
+        self.assertEqual(res.status_code, 302)
+        self.assertEqual(res.url, '/admin/login/?next=/admin/mfa/')
+
+        res = self.client.get(res.url)
+        self.assertEqual(res.status_code, 302)
+        self.assertEqual(res.url, '/login/?next=/admin/mfa/')
+
+    def test_login(self):
+        res = self.client.get('/admin/login/')
+        self.assertEqual(res.status_code, 302)
+        self.assertEqual(res.url, '/login/?next=/admin/')
+
+
 class QRCodeTest(TestCase):
     def test_is_svg(self):
         code = get_qrcode('some_data')
         self.assertTrue(code.startswith('<svg'))
         self.assertTrue(code.endswith('</svg>'))
diff --git a/tests/urls.py b/tests/urls.py
index 6701916..69a6dc4 100644
--- a/tests/urls.py
+++ b/tests/urls.py
@@ -1,5 +1,6 @@
+from django.contrib import admin
 from django.contrib.auth.decorators import login_required
 from django.contrib.auth.views import LogoutView
 from django.http import HttpResponse
 from django.urls import include
 from django.urls import path
@@ -14,7 +15,8 @@ def dummy(request):
 
 urlpatterns = [
     path('', login_required(dummy)),
     path('login/', LoginView.as_view()),
     path('logout/', public(LogoutView.as_view(next_page='/'))),
+    path('admin/', admin.site.urls),
     path('mfa/', include('mfa.urls', namespace='mfa')),
 ]

Vulnerable code: 2.  Use `mfa.views.LoginView` instead of the regular login view
Fixed code: 2.  Use `mfa.views.LoginView` instead of the regular login view. (Be sure to
    remove any other login routes, otherwise the multi factor authentication
    can be circumvented. The admin login will automatically be patched to
    redirect to the regular login.)
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth.views import redirect_to_login
from django.urls import reverse
original_login = admin.AdminSite.login


def custom_login(self, request, extra_context=None):
    next_url = (
        request.GET.get(REDIRECT_FIELD_NAME)
        or request.POST.get(REDIRECT_FIELD_NAME)
        or reverse('admin:index')
    )
    return redirect_to_login(next_url)


def patch_admin():
    setattr(admin.AdminSite, 'login', custom_login)


def unpatch_admin():
    setattr(admin.AdminSite, 'login', original_login)

from django.apps import AppConfig


class TwoFactorConfig(AppConfig):
    name = 'mfa'
    verbose_name = 'Multi Factor Authentication'

    def ready(self):
        from .admin import patch_admin
        patch_admin()
class PatchAdminTest(TestCase):
    def test_root(self):
        res = self.client.get('/admin/')
        self.assertEqual(res.status_code, 302)
        self.assertEqual(res.url, '/admin/login/?next=/admin/')

        res = self.client.get(res.url)
        self.assertEqual(res.status_code, 302)
        self.assertEqual(res.url, '/login/?next=/admin/')

    def test_app(self):
        res = self.client.get('/admin/mfa/')
        self.assertEqual(res.status_code, 302)
        self.assertEqual(res.url, '/admin/login/?next=/admin/mfa/')

        res = self.client.get(res.url)
        self.assertEqual(res.status_code, 302)
        self.assertEqual(res.url, '/login/?next=/admin/mfa/')

    def test_login(self):
        res = self.client.get('/admin/login/')
        self.assertEqual(res.status_code, 302)
        self.assertEqual(res.url, '/login/?next=/admin/')


from django.contrib import admin
    path('admin/', admin.site.urls),