CVE-2021-46898: URL Redirection to Untrusted Site ('Open Redirect') (CWE-601)
value: 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.
CWE 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.
CVSS v3 base score: 6.1
Published: 2023-10-22T19:15Z
Repository: https://github.com/sehmaschine/django-grappelli
Language: Python
Files changed: grappelli/views/switch.py
Commit date: 2021-09-30 20:39:12 +0900
Commit message: 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)
Diff: diff --git a/grappelli/views/switch.py b/grappelli/views/switch.py
index 8e390b13..db2ee680 100644
--- a/grappelli/views/switch.py
+++ b/grappelli/views/switch.py
@@ -6,10 +6,11 @@ from django.contrib.admin.views.decorators import staff_member_required
 from django.contrib.auth import load_backend, login
 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
 
 try:
@@ -26,34 +27,39 @@ def switch_user(request, object_id):
     current_user = request.user
     session_user = request.session.get("original_user", {"id": current_user.id, "username": current_user.get_username()})
 
     # 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'):
         for backend in settings.AUTHENTICATION_BACKENDS:
             if target_user == load_backend(backend).get_user(target_user.pk):
@@ -64,6 +70,6 @@ def switch_user(request, object_id):
     if hasattr(target_user, 'backend'):
         login(request, target_user)
         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)

Vulnerable code:     if redirect_url is None or not redirect_url.startswith("/"):

            return redirect(request.GET.get("redirect"))
        return redirect(request.GET.get("redirect"))
            return redirect(request.GET.get("redirect"))
        return redirect(request.GET.get("redirect"))
    return redirect(request.GET.get("redirect"))
Fixed code: from django.utils.http import url_has_allowed_host_and_scheme
    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(),
        ):
        
            return redirect(redirect_url)
        return redirect(redirect_url)
            return redirect(redirect_url)
        return redirect(redirect_url)
    return redirect(redirect_url)