CVE-2022-24857 (published 2022-04-15T19:15Z)

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)

Severity: MEDIUM
CVSS v2 base score: 6.5 (AV:N/AC:L/Au:S/C:P/I:P/A:P)
CVSS v3 base score: 8.8, HIGH (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-287
cwe_name: Improper Authentication
description: When an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct.
extended_description: 
            
			
         
         

Fix information:
repo_url: https://github.com/xi/django-mfa3
commit_details: repo_url: https://github.com/xi/django-mfa3
author: Tobias Bengfort
author_date: 2022-04-15 11:38:42+02:00
msg: fix bypass via admin login

inspired by django-two-factor-auth
repository: repo_url: https://github.com/xi/django-mfa3
repo_name: xi/django-mfa3
description: multi factor authentication for django
repo_language: Python
owner: xi
file_changes: filename: README.md
change_type: ModificationType.MODIFY
diff: @@ -21,7 +21,10 @@ 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

programming_language: Markdown
filename: admin.py
change_type: ModificationType.MODIFY
diff: @@ -1,7 +1,29 @@
 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):

programming_language: Python
filename: apps.py
change_type: ModificationType.ADD
diff: @@ -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()

programming_language: Python
filename: tests.py
change_type: ModificationType.MODIFY
diff: @@ -262,6 +262,31 @@ class MFAEnforceMiddlewareTest(MFATestCase):
         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')

programming_language: Python
filename: urls.py
change_type: ModificationType.MODIFY
diff: @@ -1,3 +1,4 @@
+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
@@ -16,5 +17,6 @@ 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')),
 ]

programming_language: Python