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