Skip to content

Commit dc2a5b5

Browse files
authored
Merge pull request #478 from jtpio/xsrf
Tighten xsrf checks
2 parents efbe484 + 88e87f8 commit dc2a5b5

File tree

3 files changed

+68
-2
lines changed

3 files changed

+68
-2
lines changed

jupyter_server/base/handlers.py

+63-1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ def content_security_policy(self):
7474

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

7980
headers["Content-Security-Policy"] = self.content_security_policy
@@ -383,13 +384,69 @@ def check_origin(self, origin_to_satisfy_tornado=""):
383384
)
384385
return allow
385386

387+
def check_referer(self):
388+
"""Check Referer for cross-site requests.
389+
Disables requests to certain endpoints with
390+
external or missing Referer.
391+
If set, allow_origin settings are applied to the Referer
392+
to whitelist specific cross-origin sites.
393+
Used on GET for api endpoints and /files/
394+
to block cross-site inclusion (XSSI).
395+
"""
396+
if self.allow_origin == "*" or self.skip_check_origin():
397+
return True
398+
399+
host = self.request.headers.get("Host")
400+
referer = self.request.headers.get("Referer")
401+
402+
if not host:
403+
self.log.warning("Blocking request with no host")
404+
return False
405+
if not referer:
406+
self.log.warning("Blocking request with no referer")
407+
return False
408+
409+
referer_url = urlparse(referer)
410+
referer_host = referer_url.netloc
411+
if referer_host == host:
412+
return True
413+
414+
# apply cross-origin checks to Referer:
415+
origin = "{}://{}".format(referer_url.scheme, referer_url.netloc)
416+
if self.allow_origin:
417+
allow = self.allow_origin == origin
418+
elif self.allow_origin_pat:
419+
allow = bool(self.allow_origin_pat.match(origin))
420+
else:
421+
# No CORS settings, deny the request
422+
allow = False
423+
424+
if not allow:
425+
self.log.warning("Blocking Cross Origin request for %s. Referer: %s, Host: %s",
426+
self.request.path, origin, host,
427+
)
428+
return allow
429+
386430
def check_xsrf_cookie(self):
387431
"""Bypass xsrf cookie checks when token-authenticated"""
388432
if self.token_authenticated or self.settings.get('disable_check_xsrf', False):
389433
# Token-authenticated requests do not need additional XSRF-check
390434
# Servers without authentication are vulnerable to XSRF
391435
return
392-
return super(JupyterHandler, self).check_xsrf_cookie()
436+
try:
437+
return super(JupyterHandler, self).check_xsrf_cookie()
438+
except web.HTTPError as e:
439+
if self.request.method in {'GET', 'HEAD'}:
440+
# Consider Referer a sufficient cross-origin check for GET requests
441+
if not self.check_referer():
442+
referer = self.request.headers.get('Referer')
443+
if referer:
444+
msg = "Blocking Cross Origin request from {}.".format(referer)
445+
else:
446+
msg = "Blocking request from unknown origin"
447+
raise web.HTTPError(403, msg)
448+
else:
449+
raise
393450

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

692+
@web.authenticated
693+
def head(self, path):
694+
self.check_xsrf_cookie()
695+
return super(AuthenticatedFileHandler, self).head(path)
696+
635697
@web.authenticated
636698
def get(self, path):
637699
if os.path.splitext(path)[1] == '.ipynb' or self.get_argument("download", False):

jupyter_server/files/handlers.py

+4
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,13 @@ def content_security_policy(self):
2929
@web.authenticated
3030
def head(self, path):
3131
self.get(path, include_body=False)
32+
self.check_xsrf_cookie()
33+
return self.get(path, include_body=False)
3234

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

3741
if await ensure_async(cm.is_hidden(path)) and not cm.allow_hidden:

jupyter_server/nbconvert/handlers.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ class NbconvertFileHandler(JupyterHandler):
8282

8383
@web.authenticated
8484
async def get(self, format, path):
85-
85+
self.check_xsrf_cookie()
8686
exporter = get_exporter(format, config=self.config, log=self.log)
8787

8888
path = path.strip('/')

0 commit comments

Comments
 (0)