Skip to content

Commit

Permalink
Merge pull request from GHSA-9f66-54xg-pc2c
Browse files Browse the repository at this point in the history
sync _redirect_safe with upstream
  • Loading branch information
Zsailer committed Dec 16, 2020
2 parents b328e0a + 20c84e8 commit 85e4abc
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 6 deletions.
19 changes: 13 additions & 6 deletions jupyter_server/auth/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,19 @@ def _redirect_safe(self, url, default=None):
"""
if default is None:
default = self.base_url
if not url.startswith(self.base_url):
# protect chrome users from mishandling unescaped backslashes.
# \ is not valid in urls, but some browsers treat it as /
# instead of %5C, causing `\\` to behave as `//`
url = url.replace("\\", "%5C")
parsed = urlparse(url)
if parsed.netloc or not (parsed.path + "/").startswith(self.base_url):
# require that next_url be absolute path within our path
allow = False
# OR pass our cross-origin check
if '://' in url:
if parsed.netloc:
# if full URL, run our cross-origin check:
parsed = urlparse(url.lower())
origin = '%s://%s' % (parsed.scheme, parsed.netloc)
origin = origin.lower()
if self.allow_origin:
allow = self.allow_origin == origin
elif self.allow_origin_pat:
Expand Down Expand Up @@ -77,9 +82,11 @@ def post(self):
self.set_login_cookie(self, uuid.uuid4().hex)
elif self.token and self.token == typed_password:
self.set_login_cookie(self, uuid.uuid4().hex)
if new_password and self.settings.get('allow_password_change'):
config_dir = self.settings.get('config_dir')
config_file = os.path.join(config_dir, 'jupyter_server_config.json')
if new_password and self.settings.get("allow_password_change"):
config_dir = self.settings.get("config_dir")
config_file = os.path.join(
config_dir, "jupyter_notebook_config.json"
)
set_password(new_password, config_file=config_file)
self.log.info("Wrote hashed password to %s" % config_file)
else:
Expand Down
95 changes: 95 additions & 0 deletions tests/auth/test_login.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"""Tests for login redirects"""

from functools import partial
from urllib.parse import urlencode

import pytest
from tornado.httpclient import HTTPClientError
from tornado.httputil import url_concat, parse_cookie

from jupyter_server.utils import url_path_join


# override default config to ensure a non-empty base url is used
@pytest.fixture
def jp_base_url():
return "/a%40b/"


@pytest.fixture
def jp_server_config(jp_base_url):
return {
"ServerApp": {
"base_url": jp_base_url,
},
}


async def _login(jp_serverapp, http_server_client, jp_base_url, next):
# first: request login page with no creds
login_url = url_path_join(jp_base_url, "login")
first = await http_server_client.fetch(login_url)
cookie_header = first.headers["Set-Cookie"]
cookies = parse_cookie(cookie_header)

# second, submit login form with credentials
try:
resp = await http_server_client.fetch(
url_concat(login_url, {"next": next}),
method="POST",
body=urlencode(
{
"password": jp_serverapp.token,
"_xsrf": cookies.get("_xsrf", ""),
}
),
headers={"Cookie": cookie_header},
follow_redirects=False,
)
except HTTPClientError as e:
if e.code != 302:
raise
return e.response.headers["Location"]
else:
assert resp.code == 302, "Should have returned a redirect!"


@pytest.fixture
def login(jp_serverapp, http_server_client, jp_base_url):
"""Fixture to return a function to login to a Jupyter server
by submitting the login page form
"""
yield partial(_login, jp_serverapp, http_server_client, jp_base_url)


@pytest.mark.parametrize(
"bad_next",
(
r"\\tree",
"//some-host",
"//host{base_url}tree",
"https://google.com",
"/absolute/not/base_url",
),
)
async def test_next_bad(login, jp_base_url, bad_next):
bad_next = bad_next.format(base_url=jp_base_url)
url = await login(bad_next)
assert url == jp_base_url


@pytest.mark.parametrize(
"next_path",
(
"tree/",
"//{base_url}tree",
"notebooks/notebook.ipynb",
"tree//something",
),
)
async def test_next_ok(login, jp_base_url, next_path):
next_path = next_path.format(base_url=jp_base_url)
expected = jp_base_url + next_path
actual = await login(next=expected)
assert actual == expected

0 comments on commit 85e4abc

Please sign in to comment.