From 88e87f815817af7799c563c7b156211ca70f4e21 Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Fri, 16 Apr 2021 10:34:56 +0200 Subject: [PATCH] Tighten xsrf checks --- jupyter_server/base/handlers.py | 64 +++++++++++++++++++++++++++- jupyter_server/files/handlers.py | 4 ++ jupyter_server/nbconvert/handlers.py | 2 +- 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/jupyter_server/base/handlers.py b/jupyter_server/base/handlers.py index c230753246..b128f7b602 100755 --- a/jupyter_server/base/handlers.py +++ b/jupyter_server/base/handlers.py @@ -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 @@ -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. @@ -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): diff --git a/jupyter_server/files/handlers.py b/jupyter_server/files/handlers.py index e73c445c65..675af809e5 100644 --- a/jupyter_server/files/handlers.py +++ b/jupyter_server/files/handlers.py @@ -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: diff --git a/jupyter_server/nbconvert/handlers.py b/jupyter_server/nbconvert/handlers.py index 550d7bace1..b9cb711a68 100644 --- a/jupyter_server/nbconvert/handlers.py +++ b/jupyter_server/nbconvert/handlers.py @@ -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('/')