@@ -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 ):
0 commit comments