CVE-2022-4589 (published 2022-12-17T13:15Z)

A vulnerability has been found in cyface Terms and Conditions Module up to 2.0.9 and classified as problematic. Affected by this vulnerability is the function returnTo of the file termsandconditions/views.py. The manipulation leads to open redirect. The attack can be launched remotely. Upgrading to version 2.0.10 is able to address this issue. The name of the patch is 03396a1c2e0af95e12a45c5faef7e47a4b513e1a. It is recommended to upgrade the affected component. The associated identifier of this vulnerability is VDB-216175.

CVSS v3 base score: 6.1, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N)

Weakness information:
cwe_id: CWE-601
cwe_name: URL Redirection to Untrusted Site ('Open Redirect')
description: A web application accepts a user-controlled input that specifies a link to an external site, and uses that link in a Redirect. This simplifies phishing attacks.
extended_description: An http parameter may contain a URL value and could cause the web application to redirect the request to the specified URL. By modifying the URL value to a malicious site, an attacker may successfully launch a phishing scam and steal user credentials. Because the server name in the modified link is identical to the original site, phishing attempts have a more trustworthy appearance. Whether this issue poses a vulnerability will be subject to the intended behavior of the application. For example, a search engine might intentionally provide redirects to arbitrary URLs.
         

Fix information:
repo_url: https://github.com/cyface/django-termsandconditions
commit_details: repo_url: https://github.com/cyface/django-termsandconditions
author: Amar Sahinovic
author_date: 2022-06-20 22:27:46+02:00
msg: Fix open redirect vulnerability
repository: repo_url: https://github.com/cyface/django-termsandconditions
repo_name: cyface/django-termsandconditions
description: Django Terms and Conditions Module
repo_language: Python
owner: cyface
file_changes: filename: tests.py
change_type: ModificationType.MODIFY
diff: @@ -242,7 +242,7 @@ class TermsAndConditionsTests(TestCase):
         )
         self.assertContains(accept_version_post_response, "Secure")
 
-    def test_accept_redirect_safe(self):
+    def _post_accept(self, return_to):
         # Pre-accept terms 2 and 3
         UserTermsAndConditions.objects.create(user=self.user1, terms=self.terms2)
         UserTermsAndConditions.objects.create(user=self.user1, terms=self.terms3)
@@ -253,19 +253,28 @@ class TermsAndConditionsTests(TestCase):
 
         LOGGER.debug("Test /terms/accept/site-terms/1/ post")
         accept_response = self.client.post(
-            "/terms/accept/", {"terms": 1, "returnTo": "/secure/"}, follow=True
+            "/terms/accept/", {"terms": 1, "returnTo": return_to}, follow=True
         )
+        return accept_response
+
+    def test_accept_redirect_safe(self):
+        accept_response = self._post_accept("/secure/")
         self.assertRedirects(accept_response, "/secure/")
 
     def test_accept_redirect_unsafe(self):
-        # Pre-accept terms 2 and 3
-        UserTermsAndConditions.objects.create(user=self.user1, terms=self.terms2)
-        UserTermsAndConditions.objects.create(user=self.user1, terms=self.terms3)
+        accept_response = self._post_accept("http://attacker/")
+        self.assertRedirects(accept_response, "/")
 
-        LOGGER.debug("Test /terms/accept/contrib-terms/3/ post")
-        accept_response = self.client.post(
-            "/terms/accept/", {"terms": 3, "returnTo": "http://attacker/"}, follow=False
-        )
+    def test_accept_redirect_unsafe_2(self):
+        accept_response = self._post_accept("//attacker.com")
+        self.assertRedirects(accept_response, "/")
+
+    def test_accept_redirect_unsafe_3(self):
+        accept_response = self._post_accept("///attacker.com")
+        self.assertRedirects(accept_response, "/")
+
+    def test_accept_redirect_unsafe_4(self):
+        accept_response = self._post_accept("////attacker.com")
         self.assertRedirects(accept_response, "/")
 
     def test_accept_store_ip_address(self):

programming_language: Python
filename: views.py
change_type: ModificationType.MODIFY
diff: @@ -14,6 +14,7 @@ from django.http import HttpResponseRedirect, Http404
 from django.utils.translation import gettext as _
 from django.views.generic import DetailView, CreateView, FormView
 from django.template.loader import get_template
+from django.utils.encoding import iri_to_uri
 import logging
 from smtplib import SMTPException
 
@@ -44,6 +45,28 @@ class GetTermsViewMixin(object):
             terms = TermsAndConditions.get_active_terms_not_agreed_to(self.request.user)
         return terms
 
+    def get_return_to(self, from_dict):
+        return_to = from_dict.get("returnTo", "/")
+
+        if self.is_safe_url(return_to):
+            # Django recommends to use this together with the helper above
+            return iri_to_uri(return_to)
+
+        LOGGER.debug("Unsafe URL found: {}".format(return_to))
+        return "/"
+
+    def is_safe_url(self, url):
+        # In Django 3.0 is_safe_url is renamed, so we import conditionally:
+        # https://docs.djangoproject.com/en/3.2/releases/3.0/#id3
+        try:
+            from django.utils.http import url_has_allowed_host_and_scheme
+        except ImportError:
+            from django.utils.http import (
+                is_safe_url as url_has_allowed_host_and_scheme,
+            )
+
+        return url_has_allowed_host_and_scheme(url, settings.ALLOWED_HOSTS)
+
 
 class AcceptTermsView(CreateView, GetTermsViewMixin):
     """
@@ -69,7 +92,7 @@ class AcceptTermsView(CreateView, GetTermsViewMixin):
         LOGGER.debug("termsandconditions.views.AcceptTermsView.get_initial")
 
         terms = self.get_terms(self.kwargs)
-        return_to = self.request.GET.get("returnTo", "/")
+        return_to = self.get_return_to(self.request.GET)
 
         return {"terms": terms, "returnTo": return_to}
 
@@ -77,14 +100,9 @@ class AcceptTermsView(CreateView, GetTermsViewMixin):
         """
         Handles POST request.
         """
-        return_url = request.POST.get("returnTo", "/")
+        return_url = self.get_return_to(self.request.POST)
         terms_ids = request.POST.getlist("terms")
 
-        parsed = urlparse(return_url)
-        if parsed.hostname and parsed.hostname not in settings.ALLOWED_HOSTS:
-            # Make sure the return url is a relative path or a trusted hostname
-            return_url = '/'
-
         if not terms_ids:  # pragma: nocover
             return HttpResponseRedirect(return_url)
 
@@ -147,7 +165,7 @@ class EmailTermsView(FormView, GetTermsViewMixin):
 
         terms = self.get_terms(self.kwargs)
 
-        return_to = self.request.GET.get("returnTo", "/")
+        return_to = self.get_return_to(self.request.GET)
 
         return {"terms": terms, "returnTo": return_to}
 
@@ -179,7 +197,7 @@ class EmailTermsView(FormView, GetTermsViewMixin):
                 _("An Error Occurred Sending Your Message."),
             )
 
-        self.success_url = form.cleaned_data.get("returnTo", "/") or "/"
+        self.success_url = self.get_return_to(form.cleaned_data)
 
         return super().form_valid(form)
 

programming_language: Python