CVE-2023-25171: Allocation of Resources Without Limits or Throttling (CWE-770)
value: Kiwi TCMS, an open source test management system, does not impose rate limits in versions prior to 12.0. This makes it easier to attempt denial-of-service attacks against the Password reset page. An attacker could potentially send a large number of emails if they know the email addresses of users in Kiwi TCMS. Additionally that may strain SMTP resources. Users should upgrade to v12.0 or later to receive a patch. As potential workarounds, users may install and configure a rate-limiting proxy in front of Kiwi TCMS and/or configure rate limits on their email server when possible.
CWE description: The product allocates a reusable resource or group of resources on behalf of an actor without imposing any restrictions on the size or number of resources that can be allocated, in violation of the intended security policy for that actor.
CVSS v3 base score: 5.9
Published: 2023-02-15T15:15Z
Repository: https://github.com/kiwitcms/Kiwi
Language: Python
Files changed: tcms/kiwi_auth/forms.py, tcms/kiwi_auth/tests/test_views.py, tcms/templates/registration/password_reset_form.html
Commit date: 2023-02-09 16:56:23 +0100
Commit message: Show captcha on password reset form if enabled

Diff: diff --git a/tcms/kiwi_auth/forms.py b/tcms/kiwi_auth/forms.py
index 9483cf814..c87160d46 100644
--- a/tcms/kiwi_auth/forms.py
+++ b/tcms/kiwi_auth/forms.py
@@ -92,10 +92,18 @@ class PasswordResetForm(
     Overrides the default form b/c it uses Site.objects.get_current()
     which uses an internal cache and produces wrong results when
     kiwitcms-tenants is installed.
     """
 
+    captcha = (
+        fields.CaptchaField(
+            widget=CustomCaptchaTextInput(attrs={"class": "form-control"})
+        )
+        if settings.USE_CAPTCHA
+        else None
+    )
+
     def save(  # pylint: disable=too-many-arguments
         self,
         domain_override=None,
         subject_template_name="registration/password_reset_subject.txt",
         email_template_name="registration/password_reset_email.html",
diff --git a/tcms/kiwi_auth/tests/test_views.py b/tcms/kiwi_auth/tests/test_views.py
index 183f07ff7..8505ba786 100644
--- a/tcms/kiwi_auth/tests/test_views.py
+++ b/tcms/kiwi_auth/tests/test_views.py
@@ -398,12 +398,29 @@ class TestPasswordResetView(TestCase):
     @patch("tcms.kiwi_auth.forms.DjangoPasswordResetForm.send_mail")
     def test_send_mail_for_password_reset(self, mail_sent):
         user = User.objects.create_user("kiwi-tester", "tester@example.com", "password")
         user.is_active = True
         user.save()
-        data = {"email": "tester@example.com"}
-        response = self.client.post(self.password_reset_url, data, follow=True)
+
+        try:
+            # https://github.com/mbi/django-simple-captcha/issues/84
+            # pylint: disable=import-outside-toplevel
+            from captcha.conf import settings as captcha_settings
+
+            captcha_settings.CAPTCHA_TEST_MODE = True
+
+            response = self.client.post(
+                self.password_reset_url,
+                {
+                    "email": "tester@example.com",
+                    "captcha_0": "PASSED",
+                    "captcha_1": "PASSED",
+                },
+                follow=True,
+            )
+        finally:
+            captcha_settings.CAPTCHA_TEST_MODE = False
 
         self.assertContains(response, _("Password reset email was sent"))
 
         # Verify mail is sent
         mail_sent.assert_called_once()
diff --git a/tcms/templates/registration/password_reset_form.html b/tcms/templates/registration/password_reset_form.html
index 6a64fc324..f61073a11 100644
--- a/tcms/templates/registration/password_reset_form.html
+++ b/tcms/templates/registration/password_reset_form.html
@@ -16,10 +16,14 @@
           <label for="inputEmail" class="col-sm-2 col-md-2 control-label">{% trans "E-mail" %}</label>
           <div class="col-sm-10 col-md-10">
             <input type="text" class="form-control" id="inputEmail" name="email" placeholder="" tabindex="1">
           </div>
         </div>
+        {% if 'captcha' in form.fields %}
+            {{ form.captcha.errors }}
+            {{ form.captcha }}
+        {% endif %}
         <div class="form-group">
           <div class="col-xs-12 col-sm-offset-2 col-sm-10 col-md-offset-2 col-md-10 submit">
             <button type="submit" class="btn btn-primary btn-lg" tabindex="4">{% trans "Password reset" %}</button>
           </div>
         </div>

Vulnerable code:         data = {"email": "tester@example.com"}
        response = self.client.post(self.password_reset_url, data, follow=True)
Fixed code:     captcha = (
        fields.CaptchaField(
            widget=CustomCaptchaTextInput(attrs={"class": "form-control"})
        )
        if settings.USE_CAPTCHA
        else None
    )


        try:
            # https://github.com/mbi/django-simple-captcha/issues/84
            # pylint: disable=import-outside-toplevel
            from captcha.conf import settings as captcha_settings

            captcha_settings.CAPTCHA_TEST_MODE = True

            response = self.client.post(
                self.password_reset_url,
                {
                    "email": "tester@example.com",
                    "captcha_0": "PASSED",
                    "captcha_1": "PASSED",
                },
                follow=True,
            )
        finally:
            captcha_settings.CAPTCHA_TEST_MODE = False
        {% if 'captcha' in form.fields %}
            {{ form.captcha.errors }}
            {{ form.captcha }}
        {% endif %}