CVE-2021-46898 (published 2023-10-22T19:15Z)

views/switch.py in django-grappelli (aka Django Grappelli) before 2.15.2 attempts to prevent external redirection with startswith("/") but this does not consider a protocol-relative URL (e.g., //example.com) attack.

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/sehmaschine/django-grappelli
commit_details: repo_url: https://github.com/sehmaschine/django-grappelli
author: ksg
author_date: 2021-09-30 20:39:12+09:00
msg: Update switch.py

This will fix issue #975 (I referred to this https://github.com/django/django/blob/main/django/views/i18n.py#L41-L45)
repository: repo_url: https://github.com/sehmaschine/django-grappelli
repo_name: sehmaschine/django-grappelli
description: A jazzy skin for the Django Admin-Interface (official repository).
repo_language: HTML
owner: sehmaschine
file_changes: filename: switch.py
change_type: ModificationType.MODIFY
diff: @@ -8,6 +8,7 @@ from django.core.exceptions import ObjectDoesNotExist
 from django.http import Http404
 from django.shortcuts import redirect
 from django.utils.html import escape
+from django.utils.http import url_has_allowed_host_and_scheme
 from django.utils.translation import gettext_lazy as _
 
 from grappelli.settings import SWITCH_USER_ORIGINAL, SWITCH_USER_TARGET
@@ -28,30 +29,35 @@ def switch_user(request, object_id):
 
     # check redirect
     redirect_url = request.GET.get("redirect", None)
-    if redirect_url is None or not redirect_url.startswith("/"):
+    if redirect_url is None or not \
+        url_has_allowed_host_and_scheme(
+            url=redirect_url,
+            allowed_hosts={request.get_host()},
+            require_https=request.is_secure(),
+        ):
         raise Http404()
-
+        
     # check original_user
     try:
         original_user = User.objects.get(pk=session_user["id"], is_staff=True)
         if not SWITCH_USER_ORIGINAL(original_user):
             messages.add_message(request, messages.ERROR, _("Permission denied."))
-            return redirect(request.GET.get("redirect"))
+            return redirect(redirect_url)
     except ObjectDoesNotExist:
         msg = _('%(name)s object with primary key %(key)r does not exist.') % {'name': "User", 'key': escape(session_user["id"])}
         messages.add_message(request, messages.ERROR, msg)
-        return redirect(request.GET.get("redirect"))
+        return redirect(redirect_url)
 
     # check new user
     try:
         target_user = User.objects.get(pk=object_id, is_staff=True)
         if target_user != original_user and not SWITCH_USER_TARGET(original_user, target_user):
             messages.add_message(request, messages.ERROR, _("Permission denied."))
-            return redirect(request.GET.get("redirect"))
+            return redirect(redirect_url)
     except ObjectDoesNotExist:
         msg = _('%(name)s object with primary key %(key)r does not exist.') % {'name': "User", 'key': escape(object_id)}
         messages.add_message(request, messages.ERROR, msg)
-        return redirect(request.GET.get("redirect"))
+        return redirect(redirect_url)
 
     # find backend
     if not hasattr(target_user, 'backend'):
@@ -66,4 +72,4 @@ def switch_user(request, object_id):
         if original_user.id != target_user.id:
             request.session["original_user"] = {"id": original_user.id, "username": original_user.get_username()}
 
-    return redirect(request.GET.get("redirect"))
+    return redirect(redirect_url)

programming_language: Python