@@ -617,12 +617,12 @@ def test_malformed_cookie(self, validate, check_port):
617617 self .wh .socket .assert_called_with ('node1' , 10000 , connect = True )
618618 self .wh .do_proxy .assert_called_with ('<socket>' )
619619
620- def test_reject_open_redirect (self ):
620+ def test_reject_open_redirect (self , url = '//example.com/%2F..' ):
621621 # This will test the behavior when an attempt is made to cause an open
622622 # redirect. It should be rejected.
623623 mock_req = mock .MagicMock ()
624624 mock_req .makefile ().readline .side_effect = [
625- b 'GET //example.com/%2F.. HTTP/1.1\r \n ' ,
625+ f 'GET { url } HTTP/1.1\r \n '. encode ( 'utf-8' ) ,
626626 b''
627627 ]
628628
@@ -647,41 +647,32 @@ def test_reject_open_redirect(self):
647647 result = output .readlines ()
648648
649649 # Verify no redirect happens and instead a 400 Bad Request is returned.
650- self .assertIn ('400 URI must not start with //' , result [0 ].decode ())
650+ # NOTE: As of python 3.10.6 there is a fix for this vulnerability,
651+ # which will cause a 301 Moved Permanently error to be returned
652+ # instead that redirects to a sanitized version of the URL with extra
653+ # leading '/' characters removed.
654+ # See https://github.com/python/cpython/issues/87389 for details.
655+ # We will consider either response to be valid for this test. This will
656+ # also help if and when the above fix gets backported to older versions
657+ # of python.
658+ errmsg = result [0 ].decode ()
659+ expected_nova = '400 URI must not start with //'
660+ expected_cpython = '301 Moved Permanently'
661+
662+ self .assertTrue (expected_nova in errmsg or expected_cpython in errmsg )
663+
664+ # If we detect the cpython fix, verify that the redirect location is
665+ # now the same url but with extra leading '/' characters removed.
666+ if expected_cpython in errmsg :
667+ location = result [3 ].decode ()
668+ location = location .removeprefix ('Location: ' ).rstrip ('\r \n ' )
669+ self .assertTrue (
670+ location .startswith ('/example.com/%2F..' ),
671+ msg = 'Redirect location is not the expected sanitized URL' ,
672+ )
651673
652674 def test_reject_open_redirect_3_slashes (self ):
653- # This will test the behavior when an attempt is made to cause an open
654- # redirect. It should be rejected.
655- mock_req = mock .MagicMock ()
656- mock_req .makefile ().readline .side_effect = [
657- b'GET ///example.com/%2F.. HTTP/1.1\r \n ' ,
658- b''
659- ]
660-
661- # Collect the response data to verify at the end. The
662- # SimpleHTTPRequestHandler writes the response data by calling the
663- # request socket sendall() method.
664- self .data = b''
665-
666- def fake_sendall (data ):
667- self .data += data
668-
669- mock_req .sendall .side_effect = fake_sendall
670-
671- client_addr = ('8.8.8.8' , 54321 )
672- mock_server = mock .MagicMock ()
673- # This specifies that the server will be able to handle requests other
674- # than only websockets.
675- mock_server .only_upgrade = False
676-
677- # Constructing a handler will process the mock_req request passed in.
678- websocketproxy .NovaProxyRequestHandler (
679- mock_req , client_addr , mock_server )
680-
681- # Verify no redirect happens and instead a 400 Bad Request is returned.
682- self .data = self .data .decode ()
683- self .assertIn ('Error code: 400' , self .data )
684- self .assertIn ('Message: URI must not start with //' , self .data )
675+ self .test_reject_open_redirect (url = '///example.com/%2F..' )
685676
686677 @mock .patch ('nova.objects.ConsoleAuthToken.validate' )
687678 def test_no_compute_rpcapi_with_invalid_token (self , mock_validate ):
0 commit comments