@@ -589,12 +589,12 @@ def test_malformed_cookie(self, validate, check_port):
589
589
self .wh .socket .assert_called_with ('node1' , 10000 , connect = True )
590
590
self .wh .do_proxy .assert_called_with ('<socket>' )
591
591
592
- def test_reject_open_redirect (self ):
592
+ def test_reject_open_redirect (self , url = '//example.com/%2F..' ):
593
593
# This will test the behavior when an attempt is made to cause an open
594
594
# redirect. It should be rejected.
595
595
mock_req = mock .MagicMock ()
596
596
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' ) ,
598
598
b''
599
599
]
600
600
@@ -619,41 +619,32 @@ def test_reject_open_redirect(self):
619
619
result = output .readlines ()
620
620
621
621
# 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
+ )
623
645
624
646
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..' )
657
648
658
649
@mock .patch ('nova.objects.ConsoleAuthToken.validate' )
659
650
def test_no_compute_rpcapi_with_invalid_token (self , mock_validate ):
0 commit comments