Reject open redirection in the console proxy

Our console proxies (novnc, serial, spice) run in a websockify server
whose request handler inherits from the python standard
SimpleHTTPRequestHandler. There is a known issue [1] in the
SimpleHTTPRequestHandler which allows open redirects by way of URLs
in the following format:

  http://vncproxy.my.domain.com//example.com/%2F..

which if visited, will redirect a user to example.com.

We can intercept a request and reject requests that pass a redirection
URL beginning with "//" by implementing the
SimpleHTTPRequestHandler.send_head() method containing the
vulnerability to reject such requests with a 400 Bad Request.

This code is copied from a patch suggested in one of the issue comments
[2].

Closes-Bug: #1927677

[1] https://bugs.python.org/issue32084
[2] https://bugs.python.org/issue32084#msg306545

Change-Id: Ie36401c782f023d1d5f2623732619105dc2cfa24
(cherry picked from commit 781612b332)
This commit is contained in:
melanie witt 2021-05-13 05:43:42 +00:00
parent a5ce4d8061
commit 4709256142
3 changed files with 76 additions and 0 deletions

View File

@ -20,6 +20,8 @@ Leverages websockify.py by Joel Martin
import copy import copy
from http import cookies as Cookie from http import cookies as Cookie
from http import HTTPStatus
import os
import socket import socket
import sys import sys
from urllib import parse as urlparse from urllib import parse as urlparse
@ -280,6 +282,27 @@ class NovaProxyRequestHandler(websockify.ProxyRequestHandler):
def socket(self, *args, **kwargs): def socket(self, *args, **kwargs):
return websockifyserver.WebSockifyServer.socket(*args, **kwargs) return websockifyserver.WebSockifyServer.socket(*args, **kwargs)
def send_head(self):
# This code is copied from this example patch:
# https://bugs.python.org/issue32084#msg306545
path = self.translate_path(self.path)
if os.path.isdir(path):
parts = urlparse.urlsplit(self.path)
if not parts.path.endswith('/'):
# redirect browser - doing basically what apache does
new_parts = (parts[0], parts[1], parts[2] + '/',
parts[3], parts[4])
new_url = urlparse.urlunsplit(new_parts)
# Browsers interpret "Location: //uri" as an absolute URI
# like "http://URI"
if new_url.startswith('//'):
self.send_error(HTTPStatus.BAD_REQUEST,
"URI must not start with //")
return None
return super(NovaProxyRequestHandler, self).send_head()
class NovaWebSocketProxy(websockify.WebSocketProxy): class NovaWebSocketProxy(websockify.WebSocketProxy):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@ -616,6 +616,40 @@ class NovaProxyRequestHandlerTestCase(test.NoDBTestCase):
self.wh.socket.assert_called_with('node1', 10000, connect=True) self.wh.socket.assert_called_with('node1', 10000, connect=True)
self.wh.do_proxy.assert_called_with('<socket>') self.wh.do_proxy.assert_called_with('<socket>')
def test_reject_open_redirect(self):
# This will test the behavior when an attempt is made to cause an open
# redirect. It should be rejected.
mock_req = mock.MagicMock()
mock_req.makefile().readline.side_effect = [
b'GET //example.com/%2F.. HTTP/1.1\r\n',
b''
]
# Collect the response data to verify at the end. The
# SimpleHTTPRequestHandler writes the response data by calling the
# request socket sendall() method.
self.data = b''
def fake_sendall(data):
self.data += data
mock_req.sendall.side_effect = fake_sendall
client_addr = ('8.8.8.8', 54321)
mock_server = mock.MagicMock()
# This specifies that the server will be able to handle requests other
# than only websockets.
mock_server.only_upgrade = False
# Constructing a handler will process the mock_req request passed in.
websocketproxy.NovaProxyRequestHandler(
mock_req, client_addr, mock_server)
# Verify no redirect happens and instead a 400 Bad Request is returned.
self.data = self.data.decode()
self.assertIn('Error code: 400', self.data)
self.assertIn('Message: URI must not start with //', self.data)
@mock.patch('nova.objects.ConsoleAuthToken.validate') @mock.patch('nova.objects.ConsoleAuthToken.validate')
def test_no_compute_rpcapi_with_invalid_token(self, mock_validate): def test_no_compute_rpcapi_with_invalid_token(self, mock_validate):
"""Tests that we don't create a ComputeAPI object until we actually """Tests that we don't create a ComputeAPI object until we actually

View File

@ -0,0 +1,19 @@
---
security:
- |
A vulnerability in the console proxies (novnc, serial, spice) that allowed
open redirection has been `patched`_. The novnc, serial, and spice console
proxies are implemented as websockify servers and the request handler
inherits from the python standard SimpleHTTPRequestHandler. There is a
`known issue`_ in the SimpleHTTPRequestHandler which allows open redirects
by way of URLs in the following format::
http://vncproxy.my.domain.com//example.com/%2F..
which if visited, will redirect a user to example.com.
The novnc, serial, and spice console proxies will now reject requests that
pass a redirection URL beginning with "//" with a 400 Bad Request.
.. _patched: https://bugs.launchpad.net/nova/+bug/1927677
.. _known issue: https://bugs.python.org/issue32084