|
def _redirect_safe(self, url, default=None): |
|
"""Redirect if url is on our PATH |
|
|
|
Full-domain redirects are allowed if they pass our CORS origin checks. |
|
|
|
Otherwise use default (self.base_url if unspecified). |
|
""" |
|
if default is None: |
|
default = 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") |
|
# urllib and browsers interpret extra '/' in the scheme separator (`scheme:///host/path`) |
|
# differently. |
|
# urllib gives scheme=scheme, netloc='', path='/host/path', while |
|
# browsers get scheme=scheme, netloc='host', path='/path' |
|
# so make sure ':///*' collapses to '://' by splitting and stripping any additional leading slash |
|
# don't allow any kind of `:/` shenanigans by splitting on ':' only |
|
# and replacing `:/*` with exactly `://` |
|
if ":" in url: |
|
scheme, _, rest = url.partition(":") |
|
url = f"{scheme}://{rest.lstrip('/')}" |
|
parsed = urlparse(url) |
|
# full url may be `//host/path` (empty scheme == same scheme as request) |
|
# or `https://host/path` |
|
# or even `https:///host/path` (invalid, but accepted and ambiguously interpreted) |
|
if (parsed.scheme or 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 parsed.scheme or parsed.netloc: |
|
# if full URL, run our cross-origin check: |
|
origin = f"{parsed.scheme}://{parsed.netloc}" |
|
origin = origin.lower() |
|
if self.allow_origin: |
|
allow = self.allow_origin == origin |
|
elif self.allow_origin_pat: |
|
allow = bool(re.match(self.allow_origin_pat, origin)) |
|
if not allow: |
|
# not allowed, use default |
|
self.log.warning("Not allowing login redirect to %r" % url) |
|
url = default |
|
self.redirect(url) |
Summary
The
?next=...URL query parameter has an open redirection vulnerability. Injupyter_server<=2.17.0, this URL query parameter allows redirection to arbitrary external domains, which can be exploited to facilitate phishing attacks on server users.Details
The vulnerability is caused by insufficient validation in the
LoginFormHandler._redirect_safe()method.jupyter_server/jupyter_server/auth/login.py
Lines 33 to 76 in 987ebdd
This vulnerability was originally reported by Noriaki Iwasaki. All discovery credit goes to them.
PoC
http://localhost:8888/login?next=///google.comgoogle.comdespite it being an external domain.The external domain passed in the
?nextparameter may be replaced with a malicious lookalike to facilitate phishing attacks. Jupyter Server deployments served on a public domain are especially vulnerable, asprod.company.commay be redirected to a look-alike URL such asprod.company.dev.Impact
This vulnerability affects all users, especially enterprise users who work with sensitive/confidential data.
Patches
Jupyter Server 2.18+
Workaround
None.