Here's an alternative to Andrew's patch, which fixes both the prefix problem and a few others.
There's a lot more churn, especially in the test suite, but all-in-all the result is a 20-line reduction. A test that prefix redirects actually work should probably be added; for now, it's been verified manually. # HG changeset patch # User Søren Løvborg <[email protected]> # Date 1442577469 -7200 # Fri Sep 18 13:57:49 2015 +0200 # Branch stable # Node ID 5d855cf256204208a7494c7206c4c6a84cb6b657 # Parent c79e4f89bfd30ce87f0e04024e990b88864d5ea4 login: always pass server-relative URLs in came_from The login controller uses the came_from query argument to determine the page to continue to after login. Previously, came_from specified only the URL path (obtained using h.url.current), and any URL query parameters were passed along as separate (additional) URL query parameters; to obtain the final redirect target, h.url was used to combine came_from with the request.GET. As of this changeset, came_from specifies both the URL path and query string (obtained using request.path_qs), which means that came_from can be used directly as the redirect target (as always, WebOb handles the task of expanding the server relative path to a fully qualified URL). This fixes a number of issues with the existing implementation: * Using h.url to combine came_from with query parameters caused the SCRIPT_NAME to incorrectly be prepended to came_from, even though it was already present. This was not a problem if the Kallithea instance was served directly from the server root ('/') as is common, but broke setups where Kallithea was served from a prefix. * Even though only server-relative came_from URLs were ever generated, the login controller allowed fully qualified URLs (URLs including scheme and server). To avoid an open HTTP redirect (CWE-601), the code included logic to prevent redirects to other servers. By requiring server-relative URLs, this logic can simply be removed. * The login code appended arbitrary, user-supplied query parameters to URLs by calling the Routes URLGenerator (h.url) with user-supplied keyword arguments. This construct is unfortunate, since url only appends _unknown_ keyword arguments as query parameters, and the parameter names could overlap with known keyword arguments, possibly affecting the generated URL in various ways. This changeset removes this usage from the login code, but other instances remain. (In practice, the damage is apparently limited to causing an Internal Server Error when going to e.g. "/_admin/login?host=foo", since WebOb returns Unicode strings and URLGenerator only allows byte strings for these keyword arguments.) diff --git a/kallithea/controllers/login.py b/kallithea/controllers/login.py --- a/kallithea/controllers/login.py +++ b/kallithea/controllers/login.py @@ -51,45 +51,28 @@ log = logging.getLogger(__name__) +def is_server_relative(url): + url = urlparse.urlsplit(url) + return not url.scheme and not url.netloc + + class LoginController(BaseController): def __before__(self): super(LoginController, self).__before__() - def _validate_came_from(self, came_from): - """Return True if came_from is valid and can and should be used""" - if not came_from: - return False - - parsed = urlparse.urlparse(came_from) - server_parsed = urlparse.urlparse(url.current()) - allowed_schemes = ['http', 'https'] - if parsed.scheme and parsed.scheme not in allowed_schemes: - log.error('Suspicious URL scheme detected %s for url %s', - parsed.scheme, parsed) - return False - if server_parsed.netloc != parsed.netloc: - log.error('Suspicious NETLOC detected %s for url %s server url ' - 'is: %s' % (parsed.netloc, parsed, server_parsed)) - return False - return True - - def _redirect_to_origin(self, origin): - '''redirect to the original page, preserving any get arguments given''' - request.GET.pop('came_from', None) - raise HTTPFound(location=url(origin, **request.GET)) - def index(self): - c.came_from = safe_str(request.GET.get('came_from', '')) - if not self._validate_came_from(c.came_from): - c.came_from = url('home') + c.came_from = safe_str(request.GET.get('came_from', '')) or url('home') + if not is_server_relative(c.came_from): + log.error('Invalid came_from (not server-relative): %r', c.came_from) + raise HTTPBadRequest() not_default = self.authuser.username != User.DEFAULT_USER ip_allowed = AuthUser.check_ip_allowed(self.authuser, self.ip_addr) # redirect if already logged in if self.authuser.is_authenticated and not_default and ip_allowed: - return self._redirect_to_origin(c.came_from) + raise HTTPFound(location=c.came_from) if request.POST: # import Login Form validator class @@ -119,7 +102,7 @@ else: log_in_user(user, c.form_result['remember'], is_external_auth=False) - return self._redirect_to_origin(c.came_from) + raise HTTPFound(location=c.came_from) return render('/login.html') diff --git a/kallithea/lib/auth.py b/kallithea/lib/auth.py --- a/kallithea/lib/auth.py +++ b/kallithea/lib/auth.py @@ -712,11 +712,12 @@ def redirect_to_login(message=None): from kallithea.lib import helpers as h - p = url.current() + p = request.path_qs if message: h.flash(h.literal(message), category='warning') log.debug('Redirecting to login page, origin: %s', p) - return redirect(url('login_home', came_from=p, **request.GET)) + return redirect(url('login_home', came_from=p)) + class LoginRequired(object): """ diff --git a/kallithea/templates/base/base.html b/kallithea/templates/base/base.html --- a/kallithea/templates/base/base.html +++ b/kallithea/templates/base/base.html @@ -294,7 +294,7 @@ <div id="quick_login"> %if c.authuser.username == 'default' or c.authuser.user_id is None: <h4>${_('Login to Your Account')}</h4> - ${h.form(h.url('login_home',came_from=h.url.current()))} + ${h.form(h.url('login_home', came_from=request.path_qs))} <div class="form"> <div class="fields"> <div class="field"> diff --git a/kallithea/templates/changeset/changeset_file_comment.html b/kallithea/templates/changeset/changeset_file_comment.html --- a/kallithea/templates/changeset/changeset_file_comment.html +++ b/kallithea/templates/changeset/changeset_file_comment.html @@ -87,7 +87,7 @@ ${h.form('')} <div class="clearfix"> <div class="comment-help"> - ${_('You need to be logged in to comment.')} <a href="${h.url('login_home',came_from=h.url.current())}">${_('Login now')}</a> + ${_('You need to be logged in to comment.')} <a href="${h.url('login_home', came_from=request.path_qs)}">${_('Login now')}</a> </div> </div> <div class="comment-button"> diff --git a/kallithea/tests/functional/test_login.py b/kallithea/tests/functional/test_login.py --- a/kallithea/tests/functional/test_login.py +++ b/kallithea/tests/functional/test_login.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- import re import time +import urllib +import urlparse import mock @@ -105,18 +107,14 @@ ('file:///etc/passwd',), ('ftp://ftp.example.com',), ('http://other.example.com/bl%C3%A5b%C3%A6rgr%C3%B8d',), + ('//evil.example.com/',), ]) def test_login_bad_came_froms(self, url_came_from): response = self.app.post(url(controller='login', action='index', came_from=url_came_from), {'username': TEST_USER_ADMIN_LOGIN, - 'password': TEST_USER_ADMIN_PASS}) - self.assertEqual(response.status, '302 Found') - self.assertEqual(response._environ['paste.testing_variables'] - ['tmpl_context'].came_from, '/') - response = response.follow() - - self.assertEqual(response.status, '200 OK') + 'password': TEST_USER_ADMIN_PASS}, + status=400) def test_login_short_password(self): response = self.app.post(url(controller='login', action='index'), @@ -133,64 +131,62 @@ response.mustcontain('Invalid username or password') - # verify that get arguments are correctly passed along login redirection + # verify that query string parameters survive login redirection @parameterized.expand([ - ({'foo':'one', 'bar':'two'}, ('foo=one', 'bar=two')), - ({'blue': u'blå'.encode('utf-8'), 'green':u'grøn'}, - ('blue=bl%C3%A5', 'green=gr%C3%B8n')), + ('?foo=one&bar=two',), + ('?blue=bl%C3%A5&green=gr%C3%B8n',), ]) - def test_redirection_to_login_form_preserves_get_args(self, args, args_encoded): - with fixture.anon_access(False): - response = self.app.get(url(controller='summary', action='index', - repo_name=HG_REPO, - **args)) - self.assertEqual(response.status, '302 Found') - for encoded in args_encoded: - self.assertIn(encoded, response.location) + def test_redirection_to_login_form_preserves_query(self, qs): + came_from = url('my_pullrequests') + qs + response = self.app.get(came_from) + self.assertEqual(response.status, '302 Found') + + self.assertIn('came_from=' + urllib.quote(came_from, ''), + response.location) @parameterized.expand([ - ({'foo':'one', 'bar':'two'}, ('foo=one', 'bar=two')), - ({'blue': u'blå'.encode('utf-8'), 'green':u'grøn'}, - ('blue=bl%C3%A5', 'green=gr%C3%B8n')), + ('?foo=one&bar=two',), + ('?blue=bl%C3%A5&green=gr%C3%B8n',), ]) - def test_login_form_preserves_get_args(self, args, args_encoded): + def test_login_form_preserves_query(self, qs): + came_from = url('/_admin/users') + qs response = self.app.get(url(controller='login', action='index', - came_from = '/_admin/users', - **args)) - for encoded in args_encoded: - self.assertIn(encoded, response.form.action) + came_from=came_from)) + + self.assertIn('came_from=' + urllib.quote(came_from, ''), + response.form.action) @parameterized.expand([ - ({'foo':'one', 'bar':'two'}, ('foo=one', 'bar=two')), - ({'blue': u'blå'.encode('utf-8'), 'green':u'grøn'}, - ('blue=bl%C3%A5', 'green=gr%C3%B8n')), + ('?foo=one&bar=two',), + ('?blue=bl%C3%A5&green=gr%C3%B8n',), ]) - def test_redirection_after_successful_login_preserves_get_args(self, args, args_encoded): + def test_redirection_after_successful_login_preserves_query(self, qs): + came_from = url('/_admin/users') + qs response = self.app.post(url(controller='login', action='index', - came_from = '/_admin/users', - **args), + came_from=came_from), {'username': TEST_USER_ADMIN_LOGIN, 'password': TEST_USER_ADMIN_PASS}) self.assertEqual(response.status, '302 Found') - for encoded in args_encoded: - self.assertIn(encoded, response.location) + + loc = urlparse.urlsplit(response.location) + path_qs = urlparse.urlunsplit(('', '', loc.path, loc.query, loc.fragment)) + self.assertEqual(came_from, path_qs) @parameterized.expand([ - ({'foo':'one', 'bar':'two'}, ('foo=one', 'bar=two')), - ({'blue': u'blå'.encode('utf-8'), 'green':u'grøn'}, - ('blue=bl%C3%A5', 'green=gr%C3%B8n')), + ('?foo=one&bar=two',), + ('?blue=bl%C3%A5&green=gr%C3%B8n',), ]) - def test_login_form_after_incorrect_login_preserves_get_args(self, args, args_encoded): + def test_login_form_after_incorrect_login_preserves_query(self, qs): + came_from = url('/_admin/users') + qs response = self.app.post(url(controller='login', action='index', - came_from = '/_admin/users', - **args), + came_from=came_from), {'username': 'error', 'password': 'test12'}) response.mustcontain('Invalid username or password') - for encoded in args_encoded: - self.assertIn(encoded, response.form.action) + self.assertIn('came_from=' + urllib.quote(came_from, ''), + response.form.action) #========================================================================== # REGISTRATIONS _______________________________________________ kallithea-general mailing list [email protected] http://lists.sfconservancy.org/mailman/listinfo/kallithea-general
