From 43ce1d814120da3ed96f2d8e616a650840fa5f21 Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Fri, 30 Sep 2022 00:05:15 +0200 Subject: [PATCH 1/8] utils: use same_origin() from authentic2 (#69740) --- mellon/utils.py | 59 ++++++++++++++++++++++++++++++++++++++++++++----- mellon/views.py | 4 ++-- 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/mellon/utils.py b/mellon/utils.py index 2518fd7..1fd6a3f 100644 --- a/mellon/utils.py +++ b/mellon/utils.py @@ -245,18 +245,67 @@ def is_nonnull(s): return '\x00' not in s +PROTOCOLS_TO_PORT = { + 'http': '80', + 'https': '443', +} + + +def netloc_to_host_port(netloc): + if not netloc: + return None, None + splitted = netloc.split(':', 1) + if len(splitted) > 1: + return splitted[0], splitted[1] + return splitted[0], None + + +def same_domain(domain1, domain2): + if domain1 == domain2: + return True + + if not domain1 or not domain2: + return False + + if domain2.startswith('.'): + # p1 is a sub-domain or the base domain + if domain1.endswith(domain2) or domain1 == domain2[1:]: + return True + return False + + def same_origin(url1, url2): - """ - Checks if two URLs are 'same-origin' + """Checks if both URL use the same domain. It understands domain patterns on url2, i.e. .example.com + matches www.example.com. + + If not scheme is given in url2, scheme compare is skipped. + If not scheme and not port are given, port compare is skipped. + The last two rules allow authorizing complete domains easily. """ p1, p2 = urlparse(url1), urlparse(url2) - if url1.startswith('/') or url2.startswith('/'): + p1_host, p1_port = netloc_to_host_port(p1.netloc) + p2_host, p2_port = netloc_to_host_port(p2.netloc) + + # url2 is relative, always same domain + if url2.startswith('/') and not url2.startswith('//'): return True + + if p2.scheme and p1.scheme != p2.scheme: + return False + + if not same_domain(p1_host, p2_host): + return False + try: - return (p1.scheme, p1.hostname, p1.port) == (p2.scheme, p2.hostname, p2.port) - except ValueError: + if (p2_port or (p1_port and p2.scheme)) and ( + (p1_port or PROTOCOLS_TO_PORT[p1.scheme]) != (p2_port or PROTOCOLS_TO_PORT[p2.scheme]) + ): + return False + except (ValueError, KeyError): return False + return True + def get_status_codes_and_message(profile): assert profile, 'missing lasso.Profile' diff --git a/mellon/views.py b/mellon/views.py index b38fb44..dd6a692 100644 --- a/mellon/views.py +++ b/mellon/views.py @@ -84,7 +84,7 @@ def check_next_url(request, next_url): except UnicodeError: log.warning('next parameter ignored, as is\'s not an ASCII string') return - if not utils.same_origin(next_url, request.build_absolute_uri()): + if not utils.same_origin(request.build_absolute_uri(), next_url): log.warning('next parameter ignored as it is not of the same origin') return return next_url @@ -725,7 +725,7 @@ class LogoutView(ProfileMixin, LogMixin, View): '''Launch a logout request to the identity provider''' next_url = request.GET.get(REDIRECT_FIELD_NAME) referer = request.headers.get('Referer') - if not referer or utils.same_origin(referer, request.build_absolute_uri()): + if not referer or utils.same_origin(request.build_absolute_uri(), referer): if hasattr(request, 'user') and request.user.is_authenticated: logout = None try: -- 2.37.2