@@ -333,7 +333,7 @@ class request_handler(NoLogRequestHandler, SimpleHTTPRequestHandler):
333333        pass 
334334
335335    def  setUp (self ):
336-         BaseTestCase .setUp (self )
336+         super () .setUp ()
337337        self .cwd  =  os .getcwd ()
338338        basetempdir  =  tempfile .gettempdir ()
339339        os .chdir (basetempdir )
@@ -361,7 +361,7 @@ def tearDown(self):
361361            except :
362362                pass 
363363        finally :
364-             BaseTestCase .tearDown (self )
364+             super () .tearDown ()
365365
366366    def  check_status_and_reason (self , response , status , data = None ):
367367        def  close_conn ():
@@ -417,6 +417,55 @@ def test_undecodable_filename(self):
417417        self .check_status_and_reason (response , HTTPStatus .OK ,
418418                                     data = os_helper .TESTFN_UNDECODABLE )
419419
420+     def  test_get_dir_redirect_location_domain_injection_bug (self ):
421+         """Ensure //evil.co/..%2f../../X does not put //evil.co/ in Location. 
422+ 
423+         //netloc/ in a Location header is a redirect to a new host. 
424+         https://github.com/python/cpython/issues/87389 
425+ 
426+         This checks that a path resolving to a directory on our server cannot 
427+         resolve into a redirect to another server. 
428+         """ 
429+         os .mkdir (os .path .join (self .tempdir , 'existing_directory' ))
430+         url  =  f'/python.org/..%2f..%2f..%2f..%2f..%2f../%0a%0d/../{ self .tempdir_name }  /existing_directory' 
431+         expected_location  =  f'{ url }  /'   # /python.org.../ single slash single prefix, trailing slash 
432+         # Canonicalizes to /tmp/tempdir_name/existing_directory which does 
433+         # exist and is a dir, triggering the 301 redirect logic. 
434+         response  =  self .request (url )
435+         self .check_status_and_reason (response , HTTPStatus .MOVED_PERMANENTLY )
436+         location  =  response .getheader ('Location' )
437+         self .assertEqual (location , expected_location , msg = 'non-attack failed!' )
438+ 
439+         # //python.org... multi-slash prefix, no trailing slash 
440+         attack_url  =  f'/{ url }  ' 
441+         response  =  self .request (attack_url )
442+         self .check_status_and_reason (response , HTTPStatus .MOVED_PERMANENTLY )
443+         location  =  response .getheader ('Location' )
444+         self .assertFalse (location .startswith ('//' ), msg = location )
445+         self .assertEqual (location , expected_location ,
446+                 msg = 'Expected Location header to start with a single / and ' 
447+                 'end with a / as this is a directory redirect.' )
448+ 
449+         # ///python.org... triple-slash prefix, no trailing slash 
450+         attack3_url  =  f'//{ url }  ' 
451+         response  =  self .request (attack3_url )
452+         self .check_status_and_reason (response , HTTPStatus .MOVED_PERMANENTLY )
453+         self .assertEqual (response .getheader ('Location' ), expected_location )
454+ 
455+         # If the second word in the http request (Request-URI for the http 
456+         # method) is a full URI, we don't worry about it, as that'll be parsed 
457+         # and reassembled as a full URI within BaseHTTPRequestHandler.send_head 
458+         # so no errant scheme-less //netloc//evil.co/ domain mixup can happen. 
459+         attack_scheme_netloc_2slash_url  =  f'https://pypi.org/{ url }  ' 
460+         expected_scheme_netloc_location  =  f'{ attack_scheme_netloc_2slash_url }  /' 
461+         response  =  self .request (attack_scheme_netloc_2slash_url )
462+         self .check_status_and_reason (response , HTTPStatus .MOVED_PERMANENTLY )
463+         location  =  response .getheader ('Location' )
464+         # We're just ensuring that the scheme and domain make it through, if 
465+         # there are or aren't multiple slashes at the start of the path that 
466+         # follows that isn't important in this Location: header. 
467+         self .assertTrue (location .startswith ('https://pypi.org/' ), msg = location )
468+ 
420469    def  test_get (self ):
421470        #constructs the path relative to the root directory of the HTTPServer 
422471        response  =  self .request (self .base_url  +  '/test' )
0 commit comments