CVE-2019-16766 (published 2019-11-29T17:15Z)

When using wagtail-2fa before 1.3.0, if someone gains access to someone's Wagtail login credentials, they can log into the CMS and bypass the 2FA check by changing the URL. They can then add a new device and gain full access to the CMS. This problem has been patched in version 1.3.0.

Severity: MEDIUM
CVSS v2 base score: 4.0 (AV:N/AC:L/Au:S/C:P/I:N/A:N)
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: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/labd/wagtail-2fa
commit_details: repo_url: https://github.com/labd/wagtail-2fa
author: Mike Dingjan
author_date: 2019-11-26 12:56:05+01:00
msg: Update changelog for 1.3.0 release
repository: repo_url: https://github.com/labd/wagtail-2fa
repo_name: labd/wagtail-2fa
description: 2 Factor Authentication for Wagtail
repo_language: Python
owner: labd
file_changes: filename: CHANGES
change_type: ModificationType.MODIFY
diff: @@ -1,3 +1,11 @@
+1.3.0 (2019.11.26)
+==================
+ - Add OtpRequiredMixin to enforce OTP checks in views
+ - Resolve possible vulnerabilities:
+    - [CWE-290](http://cwe.mitre.org/data/definitions/290.html)
+    - [CWE-20](http://cwe.mitre.org/data/definitions/20.html)
+
+
 1.2.0 (2019-10-30)
 ==================
  - Added missing deps for generation of the docs

programming_language: Markdown
repo_url: https://github.com/labd/wagtail-2fa
commit_details: repo_url: https://github.com/labd/wagtail-2fa
author: Nicole Nisters
author_date: 2019-11-21 12:29:50+01:00
msg: Allow a limited list of paths when user has device

This prevents users from adding or listing devices while they are not
yet verified and already have a registered device.
repository: repo_url: https://github.com/labd/wagtail-2fa
repo_name: labd/wagtail-2fa
description: 2 Factor Authentication for Wagtail
repo_language: Python
owner: labd
file_changes: filename: middleware.py
change_type: ModificationType.MODIFY
diff: @@ -10,6 +10,11 @@ from django_otp.middleware import OTPMiddleware as _OTPMiddleware
 
 class VerifyUserMiddleware(_OTPMiddleware):
     _allowed_url_names = [
+        "wagtail_2fa_auth",
+        "wagtailadmin_login",
+        "wagtailadmin_logout",
+    ]
+    _allowed_url_names_no_device = [
         "wagtail_2fa_auth",
         "wagtail_2fa_device_list",
         "wagtail_2fa_device_new",
@@ -66,24 +71,26 @@ class VerifyUserMiddleware(_OTPMiddleware):
             return False
 
         # Allow the user to a fixed number of paths when not verified
-        if request.path in self._allowed_paths:
+        user_has_device = django_otp.user_has_device(user, confirmed=True)
+        if request.path in self._get_allowed_paths(user_has_device):
             return False
 
         # For all other cases require that the user is verfied via otp
         return True
 
-    @property
-    def _allowed_paths(self):
-        """Return the paths the user may visit when not verified via otp
-
-        This result cannot be cached since we want to be compatible with the
-        django-hosts package. Django-hosts alters the urlconf based on the
-        hostname in the request, so the urls might exist for admin.<domain> but
-        not for www.<domain>.
+    def _get_allowed_paths(self, has_device):
+        """Return the paths the user may visit when not verified via otp.
 
+        If the user already has a registered device, return a limited set of
+        paths to prevent them from adding or listing devices to prevent them
+        from adding or listing devices.
         """
+        allowed_url_names = self._allowed_url_names
+        if not has_device:
+            allowed_url_names = self._allowed_url_names_no_device
+
         results = []
-        for route_name in self._allowed_url_names:
+        for route_name in allowed_url_names:
             try:
                 results.append(settings.WAGTAIL_MOUNT_PATH + reverse(route_name))
             except NoReverseMatch:

programming_language: Python
filename: test_middleware.py
change_type: ModificationType.MODIFY
diff: @@ -50,7 +50,7 @@ def test_superuser_dont_require_register_device(rf, superuser, settings):
 
 def test_not_specifiying_wagtail_mount_point_does_not_prepend_allowed_paths_with_wagtail_mount_path(settings):
     settings.WAGTAIL_MOUNT_PATH = ''
-    allowed_paths = VerifyUserMiddleware()._allowed_paths
+    allowed_paths = VerifyUserMiddleware()._get_allowed_paths(has_device=False)
 
     for allowed_path in allowed_paths:
         assert allowed_path.startswith('/cms')
@@ -58,7 +58,7 @@ def test_not_specifiying_wagtail_mount_point_does_not_prepend_allowed_paths_with
 
 def test_specifiying_wagtail_mount_point_does_prepend_allowed_paths_with_wagtail_mount_path(settings):
     settings.WAGTAIL_MOUNT_PATH = '/wagtail'
-    allowed_paths = VerifyUserMiddleware()._allowed_paths
+    allowed_paths = VerifyUserMiddleware()._get_allowed_paths(has_device=False)
 
     for allowed_path in allowed_paths:
         assert allowed_path.startswith(settings.WAGTAIL_MOUNT_PATH)

programming_language: Python