@@ -589,12 +589,12 @@ def test_malformed_cookie(self, validate, check_port):
589589 self .wh .socket .assert_called_with ('node1' , 10000 , connect = True )
590590 self .wh .do_proxy .assert_called_with ('<socket>' )
591591
592- def test_reject_open_redirect (self ):
592+ def test_reject_open_redirect (self , url = '//example.com/%2F..' ):
593593 # This will test the behavior when an attempt is made to cause an open
594594 # redirect. It should be rejected.
595595 mock_req = mock .MagicMock ()
596596 mock_req .makefile ().readline .side_effect = [
597- b 'GET //example.com/%2F.. HTTP/1.1\r \n ' ,
597+ f 'GET { url } HTTP/1.1\r \n '. encode ( 'utf-8' ) ,
598598 b''
599599 ]
600600
@@ -619,41 +619,32 @@ def test_reject_open_redirect(self):
619619 result = output .readlines ()
620620
621621 # Verify no redirect happens and instead a 400 Bad Request is returned.
622- self .assertIn ('400 URI must not start with //' , result [0 ].decode ())
622+ # NOTE: As of python 3.10.6 there is a fix for this vulnerability,
623+ # which will cause a 301 Moved Permanently error to be returned
624+ # instead that redirects to a sanitized version of the URL with extra
625+ # leading '/' characters removed.
626+ # See https://github.com/python/cpython/issues/87389 for details.
627+ # We will consider either response to be valid for this test. This will
628+ # also help if and when the above fix gets backported to older versions
629+ # of python.
630+ errmsg = result [0 ].decode ()
631+ expected_nova = '400 URI must not start with //'
632+ expected_cpython = '301 Moved Permanently'
633+
634+ self .assertTrue (expected_nova in errmsg or expected_cpython in errmsg )
635+
636+ # If we detect the cpython fix, verify that the redirect location is
637+ # now the same url but with extra leading '/' characters removed.
638+ if expected_cpython in errmsg :
639+ location = result [3 ].decode ()
640+ location = location .removeprefix ('Location: ' ).rstrip ('\r \n ' )
641+ self .assertTrue (
642+ location .startswith ('/example.com/%2F..' ),
643+ msg = 'Redirect location is not the expected sanitized URL' ,
644+ )
623645
624646 def test_reject_open_redirect_3_slashes (self ):
625- # This will test the behavior when an attempt is made to cause an open
626- # redirect. It should be rejected.
627- mock_req = mock .MagicMock ()
628- mock_req .makefile ().readline .side_effect = [
629- b'GET ///example.com/%2F.. HTTP/1.1\r \n ' ,
630- b''
631- ]
632-
633- # Collect the response data to verify at the end. The
634- # SimpleHTTPRequestHandler writes the response data by calling the
635- # request socket sendall() method.
636- self .data = b''
637-
638- def fake_sendall (data ):
639- self .data += data
640-
641- mock_req .sendall .side_effect = fake_sendall
642-
643- client_addr = ('8.8.8.8' , 54321 )
644- mock_server = mock .MagicMock ()
645- # This specifies that the server will be able to handle requests other
646- # than only websockets.
647- mock_server .only_upgrade = False
648-
649- # Constructing a handler will process the mock_req request passed in.
650- websocketproxy .NovaProxyRequestHandler (
651- mock_req , client_addr , mock_server )
652-
653- # Verify no redirect happens and instead a 400 Bad Request is returned.
654- self .data = self .data .decode ()
655- self .assertIn ('Error code: 400' , self .data )
656- self .assertIn ('Message: URI must not start with //' , self .data )
647+ self .test_reject_open_redirect (url = '///example.com/%2F..' )
657648
658649 @mock .patch ('nova.objects.ConsoleAuthToken.validate' )
659650 def test_no_compute_rpcapi_with_invalid_token (self , mock_validate ):
0 commit comments