Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
openSUSE:Backports:SLE-15-SP5:Update
python-django-grappelli.18201
CVE-2021-46898.patch
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
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)
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor