@@ -74,6 +74,7 @@ def content_security_policy(self):
74
74
75
75
def set_default_headers (self ):
76
76
headers = {}
77
+ headers ["X-Content-Type-Options" ] = "nosniff"
77
78
headers .update (self .settings .get ('headers' , {}))
78
79
79
80
headers ["Content-Security-Policy" ] = self .content_security_policy
@@ -383,13 +384,69 @@ def check_origin(self, origin_to_satisfy_tornado=""):
383
384
)
384
385
return allow
385
386
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
+
386
430
def check_xsrf_cookie (self ):
387
431
"""Bypass xsrf cookie checks when token-authenticated"""
388
432
if self .token_authenticated or self .settings .get ('disable_check_xsrf' , False ):
389
433
# Token-authenticated requests do not need additional XSRF-check
390
434
# Servers without authentication are vulnerable to XSRF
391
435
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
393
450
394
451
def check_host (self ):
395
452
"""Check the host header if remote access disallowed.
@@ -632,6 +689,11 @@ def content_security_policy(self):
632
689
return super (AuthenticatedFileHandler , self ).content_security_policy + \
633
690
"; sandbox allow-scripts"
634
691
692
+ @web .authenticated
693
+ def head (self , path ):
694
+ self .check_xsrf_cookie ()
695
+ return super (AuthenticatedFileHandler , self ).head (path )
696
+
635
697
@web .authenticated
636
698
def get (self , path ):
637
699
if os .path .splitext (path )[1 ] == '.ipynb' or self .get_argument ("download" , False ):
0 commit comments