@@ -49,7 +49,7 @@ class SecurityError(Exception):
4949
5050
5151@PluginManager .acceptPlugins
52- class UiRequest ( object ) :
52+ class UiRequest :
5353
5454 def __init__ (self , server , get , env , start_response ):
5555 if server :
@@ -99,8 +99,52 @@ def isDomain(self, address):
9999 def resolveDomain (self , domain ):
100100 return self .server .site_manager .resolveDomainCached (domain )
101101
102- # Call the request handler function base on path
102+ def isCrossOriginRequest (self ):
103+ """Prevent detecting sites on this 0net instance
104+
105+ In particular, we block non-user requests from other hosts as well as
106+ cross-site
107+ """
108+
109+ url = self .getRequestUrl ()
110+ fetch_mode = self .env .get ('HTTP_SEC_FETCH_MODE' )
111+ origin = self .env .get ('HTTP_ORIGIN' )
112+ referer = self .env .get ('HTTP_REFERER' )
113+
114+ # Allow all user-initiated requests
115+ if fetch_mode == 'navigate' :
116+ return False
117+
118+ # Deny requests that cannot be traced
119+ if not origin and not referer :
120+ return True
121+
122+ # Deny requests from non-0net origins
123+ if origin and not self .isSameHost (origin , url ):
124+ return True
125+
126+ # Allow non-site specific requests
127+ if self .getRequestSite () == '/' :
128+ return False
129+
130+ # Deny cross site requests
131+ if not self .isSameOrigin (referer , url ):
132+ return True
133+
134+ return False
135+
103136 def route (self , path ):
137+ """Main routing
138+
139+ If no internal action is performed, calls action[Path] from plugins
140+
141+ This behaviour is not very flexible or easy to follow, so perhaps
142+ we'd want something else..
143+ """
144+
145+ if self .isCrossOriginRequest ():
146+ return self .error404 ()
147+
104148 # Restict Ui access by ip
105149 if config .ui_restrict and self .env ['REMOTE_ADDR' ] not in config .ui_restrict :
106150 return self .error403 (details = False )
@@ -284,37 +328,43 @@ def isScriptNonceSupported(self):
284328 is_script_nonce_supported = True
285329 return is_script_nonce_supported
286330
331+ def getRequestSite (self ):
332+ """Return 0net site addr associated with current request
333+
334+ If request is site-agnostic, returns /
335+ """
336+ path = self .env ["PATH_INFO" ]
337+ match = re .match (r'(/raw)?(?P<site>/1[a-zA-Z0-9]*)' , path )
338+ if not match :
339+ match = re .match (r'(/raw)?/(?P<domain>[a-zA-Z0-9\.\-_]*)' , path )
340+ if match :
341+ domain = match .group ('domain' )
342+ if self .isDomain (domain ):
343+ addr = self .resolveDomain (domain )
344+ return '/' + addr
345+ return '/'
346+ return match .group ('site' )
347+
287348 # Send response headers
288349 def sendHeader (self , status = 200 , content_type = "text/html" , noscript = False , allow_ajax = False , script_nonce = None , extra_headers = []):
289- url = self .getRequestUrl ()
290- referer = self .env .get ('HTTP_REFERER' )
291- origin = self .env .get ('HTTP_ORIGIN' )
292- fetch_site = self .env .get ('HTTP_SEC_FETCH_SITE' )
293- fetch_mode = self .env .get ('HTTP_SEC_FETCH_MODE' )
294- not_same_ref = referer and not self .isSameHost (referer , url )
295- not_same_origin = origin and not self .isSameHost (origin , url )
296- cross_site_not_navigate = not referer and fetch_site == 'cross-site' and not fetch_mode == 'navigate'
297- if status != 404 and (not_same_ref or not_same_origin or cross_site_not_navigate ):
298- # pretend nothing is here for third-party access
299- return self .error404 ()
300-
301350 headers = {}
302351 headers ["Version" ] = "HTTP/1.1"
303352 headers ["Connection" ] = "Keep-Alive"
304353 headers ["Keep-Alive" ] = "max=25, timeout=30"
305354 headers ["X-Frame-Options" ] = "SAMEORIGIN"
355+ headers ["Referrer-Policy" ] = "same-origin"
306356
307357 if noscript :
308358 headers ["Content-Security-Policy" ] = "default-src 'none'; sandbox allow-top-navigation allow-forms; img-src *; font-src * data:; media-src *; style-src * 'unsafe-inline';"
309359 elif script_nonce and self .isScriptNonceSupported ():
310- headers ["Content-Security-Policy" ] = "default-src 'none'; script-src 'nonce-{0 }'; img-src 'self' blob: data:; style-src 'self' blob: 'unsafe-inline'; connect-src *; frame-src 'self' blob:" . format ( script_nonce )
360+ headers ["Content-Security-Policy" ] = f "default-src 'none'; script-src 'nonce-{ script_nonce } '; img-src 'self' blob: data:; style-src 'self' blob: 'unsafe-inline'; connect-src *; frame-src 'self' blob:"
311361
312362 if allow_ajax :
313363 headers ["Access-Control-Allow-Origin" ] = "null"
314364
315365 if self .env ["REQUEST_METHOD" ] == "OPTIONS" :
316366 # Allow json access
317- headers ["Access-Control-Allow-Headers" ] = "Origin, X-Requested-With, Content-Type, Accept, Cookie, Range"
367+ headers ["Access-Control-Allow-Headers" ] = "Origin, X-Requested-With, Content-Type, Accept, Cookie, Range, Referer "
318368 headers ["Access-Control-Allow-Credentials" ] = "true"
319369
320370 # Download instead of display file types that can be dangerous
@@ -624,15 +674,18 @@ def isSameHost(self, url_a, url_b):
624674 if not url_a or not url_b :
625675 return False
626676
627- url_a = url_a .replace ("/raw/" , "/" )
628- url_b = url_b .replace ("/raw/" , "/" )
677+ host_pattern = r'(?P<host>http[s]?://.*?)(/|$)'
629678
630- origin_pattern = "http[s]{0,1}://(.*?/).*"
679+ match_a = re .match (host_pattern , url_a )
680+ match_b = re .match (host_pattern , url_b )
631681
632- origin_a = re . sub ( origin_pattern , " \\ 1" , url_a )
633- origin_b = re . sub ( origin_pattern , " \\ 1" , url_b )
682+ if not match_a or not match_b :
683+ return False
634684
635- return origin_a == origin_b
685+ host_a = match_a .group ('host' )
686+ host_b = match_b .group ('host' )
687+
688+ return host_a == host_b
636689
637690 def isSameOrigin (self , url_a , url_b ):
638691 """Check if 0net origin is the same"""
0 commit comments