Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tighten xsrf checks #478

Merged
merged 1 commit into from
Apr 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 63 additions & 1 deletion jupyter_server/base/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ def content_security_policy(self):

def set_default_headers(self):
headers = {}
headers["X-Content-Type-Options"] = "nosniff"
headers.update(self.settings.get('headers', {}))

headers["Content-Security-Policy"] = self.content_security_policy
Expand Down Expand Up @@ -383,13 +384,69 @@ def check_origin(self, origin_to_satisfy_tornado=""):
)
return allow

def check_referer(self):
"""Check Referer for cross-site requests.
Disables requests to certain endpoints with
external or missing Referer.
If set, allow_origin settings are applied to the Referer
to whitelist specific cross-origin sites.
Used on GET for api endpoints and /files/
to block cross-site inclusion (XSSI).
"""
if self.allow_origin == "*" or self.skip_check_origin():
return True

host = self.request.headers.get("Host")
referer = self.request.headers.get("Referer")

if not host:
self.log.warning("Blocking request with no host")
return False
if not referer:
self.log.warning("Blocking request with no referer")
return False

referer_url = urlparse(referer)
referer_host = referer_url.netloc
if referer_host == host:
return True

# apply cross-origin checks to Referer:
origin = "{}://{}".format(referer_url.scheme, referer_url.netloc)
if self.allow_origin:
allow = self.allow_origin == origin
elif self.allow_origin_pat:
allow = bool(self.allow_origin_pat.match(origin))
else:
# No CORS settings, deny the request
allow = False

if not allow:
self.log.warning("Blocking Cross Origin request for %s. Referer: %s, Host: %s",
self.request.path, origin, host,
)
return allow

def check_xsrf_cookie(self):
"""Bypass xsrf cookie checks when token-authenticated"""
if self.token_authenticated or self.settings.get('disable_check_xsrf', False):
# Token-authenticated requests do not need additional XSRF-check
# Servers without authentication are vulnerable to XSRF
return
return super(JupyterHandler, self).check_xsrf_cookie()
try:
return super(JupyterHandler, self).check_xsrf_cookie()
except web.HTTPError as e:
if self.request.method in {'GET', 'HEAD'}:
# Consider Referer a sufficient cross-origin check for GET requests
if not self.check_referer():
referer = self.request.headers.get('Referer')
if referer:
msg = "Blocking Cross Origin request from {}.".format(referer)
else:
msg = "Blocking request from unknown origin"
raise web.HTTPError(403, msg)
else:
raise

def check_host(self):
"""Check the host header if remote access disallowed.
Expand Down Expand Up @@ -632,6 +689,11 @@ def content_security_policy(self):
return super(AuthenticatedFileHandler, self).content_security_policy + \
"; sandbox allow-scripts"

@web.authenticated
def head(self, path):
self.check_xsrf_cookie()
return super(AuthenticatedFileHandler, self).head(path)

@web.authenticated
def get(self, path):
if os.path.splitext(path)[1] == '.ipynb' or self.get_argument("download", False):
Expand Down
4 changes: 4 additions & 0 deletions jupyter_server/files/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,13 @@ def content_security_policy(self):
@web.authenticated
def head(self, path):
self.get(path, include_body=False)
self.check_xsrf_cookie()
return self.get(path, include_body=False)

@web.authenticated
async def get(self, path, include_body=True):
# /files/ requests must originate from the same site
self.check_xsrf_cookie()
cm = self.contents_manager

if await ensure_async(cm.is_hidden(path)) and not cm.allow_hidden:
Expand Down
2 changes: 1 addition & 1 deletion jupyter_server/nbconvert/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ class NbconvertFileHandler(JupyterHandler):

@web.authenticated
async def get(self, format, path):

self.check_xsrf_cookie()
exporter = get_exporter(format, config=self.config, log=self.log)

path = path.strip('/')
Expand Down