@@ -617,12 +617,12 @@ def test_malformed_cookie(self, validate, check_port):
617
617
self .wh .socket .assert_called_with ('node1' , 10000 , connect = True )
618
618
self .wh .do_proxy .assert_called_with ('<socket>' )
619
619
620
- def test_reject_open_redirect (self ):
620
+ def test_reject_open_redirect (self , url = '//example.com/%2F..' ):
621
621
# This will test the behavior when an attempt is made to cause an open
622
622
# redirect. It should be rejected.
623
623
mock_req = mock .MagicMock ()
624
624
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' ) ,
626
626
b''
627
627
]
628
628
@@ -647,41 +647,32 @@ def test_reject_open_redirect(self):
647
647
result = output .readlines ()
648
648
649
649
# 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
+ )
651
673
652
674
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..' )
685
676
686
677
@mock .patch ('nova.objects.ConsoleAuthToken.validate' )
687
678
def test_no_compute_rpcapi_with_invalid_token (self , mock_validate ):
0 commit comments