File CVE-2021-46898.patch of Package python-django-grappelli.18201

From 4ca94bcda0fa2720594506853d85e00c8212968f Mon Sep 17 00:00:00 2001
From: ksg <ksg97031@gmail.com>
Date: Thu, 30 Sep 2021 20:39:12 +0900
Subject: [PATCH] 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)
---
 grappelli/views/switch.py | 20 +++++++++++++-------
 1 file changed, 13 insertions(+), 7 deletions(-)

Index: django-grappelli-2.14.4/grappelli/views/switch.py
===================================================================
--- django-grappelli-2.14.4.orig/grappelli/views/switch.py
+++ django-grappelli-2.14.4/grappelli/views/switch.py
@@ -1,5 +1,7 @@
 # coding: utf-8
 
+import unicodedata
+
 from django.conf import settings
 from django.contrib import messages
 from django.contrib.admin.views.decorators import staff_member_required
@@ -9,6 +11,8 @@ from django.http import Http404
 from django.shortcuts import redirect
 from django.utils.html import escape
 from django.utils.translation import gettext_lazy as _
+from urllib.parse import _coerce_args
+from urllib.parse import ParseResult, SplitResult, uses_params
 
 from grappelli.settings import SWITCH_USER_ORIGINAL, SWITCH_USER_TARGET
 
@@ -18,6 +22,112 @@ try:
 except ImportError:
     from django.contrib.auth.models import User
 
+def url_has_allowed_host_and_scheme(url, allowed_hosts, require_https=False):
+    """
+    Return ``True`` if the url uses an allowed host and a safe scheme.
+
+    Always return ``False`` on an empty url.
+
+    If ``require_https`` is ``True``, only 'https' will be considered a valid
+    scheme, as opposed to 'http' and 'https' with the default, ``False``.
+
+    Note: "True" doesn't entail that a URL is "safe". It may still be e.g.
+    quoted incorrectly. Ensure to also use django.utils.encoding.iri_to_uri()
+    on the path component of untrusted URLs.
+    """
+    if url is not None:
+        url = url.strip()
+    if not url:
+        return False
+    if allowed_hosts is None:
+        allowed_hosts = set()
+    elif isinstance(allowed_hosts, str):
+        allowed_hosts = {allowed_hosts}
+    # Chrome treats \ completely as / in paths but it could be part of some
+    # basic auth credentials so we need to check both URLs.
+    return (
+        _url_has_allowed_host_and_scheme(url, allowed_hosts, require_https=require_https) and
+        _url_has_allowed_host_and_scheme(url.replace('\\', '/'), allowed_hosts, require_https=require_https)
+    )
+
+
+def _url_has_allowed_host_and_scheme(url, allowed_hosts, require_https=False):
+    # Chrome considers any URL with more than two slashes to be absolute, but
+    # urlparse is not so flexible. Treat any url with three slashes as unsafe.
+    if url.startswith('///'):
+        return False
+    try:
+        url_info = _urlparse(url)
+    except ValueError:  # e.g. invalid IPv6 addresses
+        return False
+    # Forbid URLs like http:///example.com - with a scheme, but without a hostname.
+    # In that URL, example.com is not the hostname but, a path component. However,
+    # Chrome will still consider example.com to be the hostname, so we must not
+    # allow this syntax.
+    if not url_info.netloc and url_info.scheme:
+        return False
+    # Forbid URLs that start with control characters. Some browsers (like
+    # Chrome) ignore quite a few control characters at the start of a
+    # URL and might consider the URL as scheme relative.
+    if unicodedata.category(url[0])[0] == 'C':
+        return False
+    scheme = url_info.scheme
+    # Consider URLs without a scheme (e.g. //example.com/p) to be http.
+    if not url_info.scheme and url_info.netloc:
+        scheme = 'http'
+    valid_schemes = ['https'] if require_https else ['http', 'https']
+    return ((not url_info.netloc or url_info.netloc in allowed_hosts) and
+            (not scheme or scheme in valid_schemes))
+
+
+# Copied from urllib.parse.urlparse() but uses fixed urlsplit() function.
+def _urlparse(url, scheme='', allow_fragments=True):
+    """Parse a URL into 6 components:
+    <scheme>://<netloc>/<path>;<params>?<query>#<fragment>
+    Return a 6-tuple: (scheme, netloc, path, params, query, fragment).
+    Note that we don't break the components up in smaller bits
+    (e.g. netloc is a single string) and we don't expand % escapes."""
+    url, scheme, _coerce_result = _coerce_args(url, scheme)
+    splitresult = _urlsplit(url, scheme, allow_fragments)
+    scheme, netloc, url, query, fragment = splitresult
+    if scheme in uses_params and ';' in url:
+        url, params = _splitparams(url)
+    else:
+        params = ''
+    result = ParseResult(scheme, netloc, url, params, query, fragment)
+    return _coerce_result(result)
+
+
+# Copied from urllib.parse.urlsplit() with
+# https://github.com/python/cpython/pull/661 applied.
+def _urlsplit(url, scheme='', allow_fragments=True):
+    """Parse a URL into 5 components:
+    <scheme>://<netloc>/<path>?<query>#<fragment>
+    Return a 5-tuple: (scheme, netloc, path, query, fragment).
+    Note that we don't break the components up in smaller bits
+    (e.g. netloc is a single string) and we don't expand % escapes."""
+    url, scheme, _coerce_result = _coerce_args(url, scheme)
+    netloc = query = fragment = ''
+    i = url.find(':')
+    if i > 0:
+        for c in url[:i]:
+            if c not in scheme_chars:
+                break
+        else:
+            scheme, url = url[:i].lower(), url[i + 1:]
+
+    if url[:2] == '//':
+        netloc, url = _splitnetloc(url, 2)
+        if (('[' in netloc and ']' not in netloc) or
+                (']' in netloc and '[' not in netloc)):
+            raise ValueError("Invalid IPv6 URL")
+    if allow_fragments and '#' in url:
+        url, fragment = url.split('#', 1)
+    if '?' in url:
+        url, query = url.split('?', 1)
+    v = SplitResult(scheme, netloc, url, query, fragment)
+    return _coerce_result(v)
+
 
 @staff_member_required
 def switch_user(request, object_id):
@@ -28,7 +138,12 @@ 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
@@ -36,22 +151,22 @@ def switch_user(request, object_id):
         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 +181,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)
openSUSE Build Service is sponsored by