diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py index 11838f55..00fb8bbd 100644 --- a/jupyterhub/handlers/base.py +++ b/jupyterhub/handlers/base.py @@ -436,15 +436,23 @@ class BaseHandler(RequestHandler): ) ): # treat absolute URLs for our host as absolute paths: + # below, redirects that aren't strictly paths parsed = urlparse(next_url) next_url = parsed.path if parsed.query: next_url = next_url + '?' + parsed.query - if parsed.hash: - next_url = next_url + '#' + parsed.hash - if next_url and (urlparse(next_url).netloc or not next_url.startswith('/')): + if parsed.fragment: + next_url = next_url + '#' + parsed.fragment + + # if it still has host info, it didn't match our above check for *this* host + if next_url and ( + '://' in next_url + or next_url.startswith('//') + or not next_url.startswith('/') + ): self.log.warning("Disallowing redirect outside JupyterHub: %r", next_url) next_url = '' + if next_url and next_url.startswith(url_path_join(self.base_url, 'user/')): # add /hub/ prefix, to ensure we redirect to the right user's server. # The next request will be handled by SpawnHandler, diff --git a/jupyterhub/tests/test_pages.py b/jupyterhub/tests/test_pages.py index fad6960c..84bf6b11 100644 --- a/jupyterhub/tests/test_pages.py +++ b/jupyterhub/tests/test_pages.py @@ -409,10 +409,13 @@ def test_login_strip(app): (False, '/has?query#andhash', '/has?query#andhash'), # next_url outside is not allowed + (False, 'relative/path', ''), (False, 'https://other.domain', ''), (False, 'ftp://other.domain', ''), (False, '//other.domain', ''), - ] + (False, '///other.domain/triple', ''), + (False, '\\\\other.domain/backslashes', ''), + ], ) @pytest.mark.gen_test def test_login_redirect(app, running, next_url, location): @@ -426,7 +429,7 @@ def test_login_redirect(app, running, next_url, location): url = 'login' if next_url: - if '//' not in next_url: + if '//' not in next_url and next_url.startswith('/'): next_url = ujoin(app.base_url, next_url, '') url = url_concat(url, dict(next=next_url))