ssl: IMPORTANT DoS FIX do_handshake_connect=False in server accept(); Thanks to Garth Mollett
Default is: accept - handshake (blocking) - return new connection Now: accept - return new connection, some time later handshake will be done implicitly Usual server code: while server.alive: conn, addr = listener.accept() server.pool.spawn(server.process, conn, addr) is vulnerable to a simple DoS attack, where evil client connects to HTTPS socket and does not perform handshake, thus blocking server in `accept()` so no other clients can be accepted.
This commit is contained in:
@@ -3,16 +3,17 @@ __ssl = __import__('ssl')
|
|||||||
from eventlet.patcher import slurp_properties
|
from eventlet.patcher import slurp_properties
|
||||||
slurp_properties(__ssl, globals(), srckeys=dir(__ssl))
|
slurp_properties(__ssl, globals(), srckeys=dir(__ssl))
|
||||||
|
|
||||||
|
import errno
|
||||||
import functools
|
import functools
|
||||||
import sys
|
import sys
|
||||||
import errno
|
import time
|
||||||
time = __import__('time')
|
|
||||||
|
|
||||||
from eventlet.support import get_errno, PY33, six
|
from eventlet import greenio
|
||||||
from eventlet.hubs import trampoline, IOClosed
|
|
||||||
from eventlet.greenio import (
|
from eventlet.greenio import (
|
||||||
set_nonblocking, GreenSocket, SOCKET_CLOSED, CONNECT_ERR, CONNECT_SUCCESS,
|
set_nonblocking, GreenSocket, CONNECT_ERR, CONNECT_SUCCESS,
|
||||||
)
|
)
|
||||||
|
from eventlet.hubs import trampoline, IOClosed
|
||||||
|
from eventlet.support import get_errno, PY33, six
|
||||||
orig_socket = __import__('socket')
|
orig_socket = __import__('socket')
|
||||||
socket = orig_socket.socket
|
socket = orig_socket.socket
|
||||||
if sys.version_info >= (2, 7):
|
if sys.version_info >= (2, 7):
|
||||||
@@ -181,10 +182,11 @@ class GreenSSLSocket(_original_sslsocket):
|
|||||||
except orig_socket.error as e:
|
except orig_socket.error as e:
|
||||||
if self.act_non_blocking:
|
if self.act_non_blocking:
|
||||||
raise
|
raise
|
||||||
if get_errno(e) == errno.EWOULDBLOCK:
|
erno = get_errno(e)
|
||||||
|
if erno in greenio.SOCKET_BLOCKING:
|
||||||
trampoline(self, write=True,
|
trampoline(self, write=True,
|
||||||
timeout=self.gettimeout(), timeout_exc=timeout_exc('timed out'))
|
timeout=self.gettimeout(), timeout_exc=timeout_exc('timed out'))
|
||||||
if get_errno(e) in SOCKET_CLOSED:
|
elif erno in greenio.SOCKET_CLOSED:
|
||||||
return ''
|
return ''
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@@ -204,14 +206,15 @@ class GreenSSLSocket(_original_sslsocket):
|
|||||||
except orig_socket.error as e:
|
except orig_socket.error as e:
|
||||||
if self.act_non_blocking:
|
if self.act_non_blocking:
|
||||||
raise
|
raise
|
||||||
if get_errno(e) == errno.EWOULDBLOCK:
|
erno = get_errno(e)
|
||||||
|
if erno in greenio.SOCKET_BLOCKING:
|
||||||
try:
|
try:
|
||||||
trampoline(
|
trampoline(
|
||||||
self, read=True,
|
self, read=True,
|
||||||
timeout=self.gettimeout(), timeout_exc=timeout_exc('timed out'))
|
timeout=self.gettimeout(), timeout_exc=timeout_exc('timed out'))
|
||||||
except IOClosed:
|
except IOClosed:
|
||||||
return b''
|
return b''
|
||||||
if get_errno(e) in SOCKET_CLOSED:
|
elif erno in greenio.SOCKET_CLOSED:
|
||||||
return b''
|
return b''
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@@ -318,7 +321,7 @@ class GreenSSLSocket(_original_sslsocket):
|
|||||||
set_nonblocking(newsock)
|
set_nonblocking(newsock)
|
||||||
break
|
break
|
||||||
except orig_socket.error as e:
|
except orig_socket.error as e:
|
||||||
if get_errno(e) != errno.EWOULDBLOCK:
|
if get_errno(e) not in greenio.SOCKET_BLOCKING:
|
||||||
raise
|
raise
|
||||||
trampoline(self, read=True, timeout=self.gettimeout(),
|
trampoline(self, read=True, timeout=self.gettimeout(),
|
||||||
timeout_exc=timeout_exc('timed out'))
|
timeout_exc=timeout_exc('timed out'))
|
||||||
@@ -331,7 +334,7 @@ class GreenSSLSocket(_original_sslsocket):
|
|||||||
cert_reqs=self.cert_reqs,
|
cert_reqs=self.cert_reqs,
|
||||||
ssl_version=self.ssl_version,
|
ssl_version=self.ssl_version,
|
||||||
ca_certs=self.ca_certs,
|
ca_certs=self.ca_certs,
|
||||||
do_handshake_on_connect=self.do_handshake_on_connect,
|
do_handshake_on_connect=False,
|
||||||
suppress_ragged_eofs=self.suppress_ragged_eofs)
|
suppress_ragged_eofs=self.suppress_ragged_eofs)
|
||||||
return (new_ssl, addr)
|
return (new_ssl, addr)
|
||||||
|
|
||||||
|
@@ -11,7 +11,7 @@ from eventlet.support import get_errno, six
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'GreenSocket', '_GLOBAL_DEFAULT_TIMEOUT', 'set_nonblocking',
|
'GreenSocket', '_GLOBAL_DEFAULT_TIMEOUT', 'set_nonblocking',
|
||||||
'SOCKET_CLOSED', 'CONNECT_ERR', 'CONNECT_SUCCESS',
|
'SOCKET_BLOCKING', 'SOCKET_CLOSED', 'CONNECT_ERR', 'CONNECT_SUCCESS',
|
||||||
'shutdown_safe', 'SSL',
|
'shutdown_safe', 'SSL',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import socket
|
import socket
|
||||||
import warnings
|
import warnings
|
||||||
from unittest import main
|
|
||||||
|
|
||||||
import eventlet
|
import eventlet
|
||||||
from eventlet import greenio
|
from eventlet import greenio
|
||||||
@@ -8,22 +7,23 @@ try:
|
|||||||
from eventlet.green import ssl
|
from eventlet.green import ssl
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
from tests import (
|
import tests
|
||||||
LimitedTestCase, certificate_file, private_key_file, check_idle_cpu_usage,
|
|
||||||
skip_if_no_ssl
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def listen_ssl_socket(address=('127.0.0.1', 0)):
|
def listen_ssl_socket(address=('localhost', 0), **kwargs):
|
||||||
sock = ssl.wrap_socket(
|
sock = ssl.wrap_socket(
|
||||||
socket.socket(), private_key_file, certificate_file, server_side=True)
|
socket.socket(),
|
||||||
|
tests.private_key_file,
|
||||||
|
tests.certificate_file,
|
||||||
|
server_side=True,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
sock.bind(address)
|
sock.bind(address)
|
||||||
sock.listen(50)
|
sock.listen(50)
|
||||||
|
|
||||||
return sock
|
return sock
|
||||||
|
|
||||||
|
|
||||||
class SSLTest(LimitedTestCase):
|
class SSLTest(tests.LimitedTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
# disabling socket.ssl warnings because we're testing it here
|
# disabling socket.ssl warnings because we're testing it here
|
||||||
warnings.filterwarnings(
|
warnings.filterwarnings(
|
||||||
@@ -33,30 +33,29 @@ class SSLTest(LimitedTestCase):
|
|||||||
|
|
||||||
super(SSLTest, self).setUp()
|
super(SSLTest, self).setUp()
|
||||||
|
|
||||||
@skip_if_no_ssl
|
@tests.skip_if_no_ssl
|
||||||
def test_duplex_response(self):
|
def test_duplex_response(self):
|
||||||
def serve(listener):
|
def serve(listener):
|
||||||
sock, addr = listener.accept()
|
sock, addr = listener.accept()
|
||||||
sock.read(8192)
|
sock.recv(8192)
|
||||||
sock.write(b'response')
|
sock.sendall(b'response')
|
||||||
|
|
||||||
sock = listen_ssl_socket()
|
sock = listen_ssl_socket()
|
||||||
|
|
||||||
server_coro = eventlet.spawn(serve, sock)
|
server_coro = eventlet.spawn(serve, sock)
|
||||||
|
|
||||||
client = ssl.wrap_socket(
|
client = ssl.wrap_socket(eventlet.connect(sock.getsockname()))
|
||||||
eventlet.connect(('127.0.0.1', sock.getsockname()[1])))
|
client.sendall(b'line 1\r\nline 2\r\n\r\n')
|
||||||
client.write(b'line 1\r\nline 2\r\n\r\n')
|
self.assertEqual(client.recv(8192), b'response')
|
||||||
self.assertEqual(client.read(8192), b'response')
|
|
||||||
server_coro.wait()
|
server_coro.wait()
|
||||||
|
|
||||||
@skip_if_no_ssl
|
@tests.skip_if_no_ssl
|
||||||
def test_ssl_close(self):
|
def test_ssl_close(self):
|
||||||
def serve(listener):
|
def serve(listener):
|
||||||
sock, addr = listener.accept()
|
sock, addr = listener.accept()
|
||||||
sock.read(8192)
|
sock.recv(8192)
|
||||||
try:
|
try:
|
||||||
self.assertEqual(b"", sock.read(8192))
|
self.assertEqual(b'', sock.recv(8192))
|
||||||
except greenio.SSL.ZeroReturnError:
|
except greenio.SSL.ZeroReturnError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -64,54 +63,54 @@ class SSLTest(LimitedTestCase):
|
|||||||
|
|
||||||
server_coro = eventlet.spawn(serve, sock)
|
server_coro = eventlet.spawn(serve, sock)
|
||||||
|
|
||||||
raw_client = eventlet.connect(('127.0.0.1', sock.getsockname()[1]))
|
raw_client = eventlet.connect(sock.getsockname())
|
||||||
client = ssl.wrap_socket(raw_client)
|
client = ssl.wrap_socket(raw_client)
|
||||||
client.write(b'X')
|
client.sendall(b'X')
|
||||||
greenio.shutdown_safe(client)
|
greenio.shutdown_safe(client)
|
||||||
client.close()
|
client.close()
|
||||||
server_coro.wait()
|
server_coro.wait()
|
||||||
|
|
||||||
@skip_if_no_ssl
|
@tests.skip_if_no_ssl
|
||||||
def test_ssl_connect(self):
|
def test_ssl_connect(self):
|
||||||
def serve(listener):
|
def serve(listener):
|
||||||
sock, addr = listener.accept()
|
sock, addr = listener.accept()
|
||||||
sock.read(8192)
|
sock.recv(8192)
|
||||||
sock = listen_ssl_socket()
|
sock = listen_ssl_socket()
|
||||||
server_coro = eventlet.spawn(serve, sock)
|
server_coro = eventlet.spawn(serve, sock)
|
||||||
|
|
||||||
raw_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
raw_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
ssl_client = ssl.wrap_socket(raw_client)
|
ssl_client = ssl.wrap_socket(raw_client)
|
||||||
ssl_client.connect(('127.0.0.1', sock.getsockname()[1]))
|
ssl_client.connect(sock.getsockname())
|
||||||
ssl_client.write(b'abc')
|
ssl_client.sendall(b'abc')
|
||||||
greenio.shutdown_safe(ssl_client)
|
greenio.shutdown_safe(ssl_client)
|
||||||
ssl_client.close()
|
ssl_client.close()
|
||||||
server_coro.wait()
|
server_coro.wait()
|
||||||
|
|
||||||
@skip_if_no_ssl
|
@tests.skip_if_no_ssl
|
||||||
def test_ssl_unwrap(self):
|
def test_ssl_unwrap(self):
|
||||||
def serve():
|
def serve():
|
||||||
sock, addr = listener.accept()
|
sock, addr = listener.accept()
|
||||||
self.assertEqual(sock.recv(6), b'before')
|
self.assertEqual(sock.recv(6), b'before')
|
||||||
sock_ssl = ssl.wrap_socket(sock, private_key_file, certificate_file,
|
sock_ssl = ssl.wrap_socket(sock, tests.private_key_file, tests.certificate_file,
|
||||||
server_side=True)
|
server_side=True)
|
||||||
sock_ssl.do_handshake()
|
sock_ssl.do_handshake()
|
||||||
self.assertEqual(sock_ssl.read(6), b'during')
|
self.assertEqual(sock_ssl.recv(6), b'during')
|
||||||
sock2 = sock_ssl.unwrap()
|
sock2 = sock_ssl.unwrap()
|
||||||
self.assertEqual(sock2.recv(5), b'after')
|
self.assertEqual(sock2.recv(5), b'after')
|
||||||
sock2.close()
|
sock2.close()
|
||||||
|
|
||||||
listener = eventlet.listen(('127.0.0.1', 0))
|
listener = eventlet.listen(('127.0.0.1', 0))
|
||||||
server_coro = eventlet.spawn(serve)
|
server_coro = eventlet.spawn(serve)
|
||||||
client = eventlet.connect((listener.getsockname()))
|
client = eventlet.connect(listener.getsockname())
|
||||||
client.send(b'before')
|
client.sendall(b'before')
|
||||||
client_ssl = ssl.wrap_socket(client)
|
client_ssl = ssl.wrap_socket(client)
|
||||||
client_ssl.do_handshake()
|
client_ssl.do_handshake()
|
||||||
client_ssl.write(b'during')
|
client_ssl.sendall(b'during')
|
||||||
client2 = client_ssl.unwrap()
|
client2 = client_ssl.unwrap()
|
||||||
client2.send(b'after')
|
client2.sendall(b'after')
|
||||||
server_coro.wait()
|
server_coro.wait()
|
||||||
|
|
||||||
@skip_if_no_ssl
|
@tests.skip_if_no_ssl
|
||||||
def test_sendall_cpu_usage(self):
|
def test_sendall_cpu_usage(self):
|
||||||
"""SSL socket.sendall() busy loop
|
"""SSL socket.sendall() busy loop
|
||||||
|
|
||||||
@@ -132,8 +131,8 @@ class SSLTest(LimitedTestCase):
|
|||||||
def serve(listener):
|
def serve(listener):
|
||||||
conn, _ = listener.accept()
|
conn, _ = listener.accept()
|
||||||
conn.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, BUFFER_SIZE)
|
conn.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, BUFFER_SIZE)
|
||||||
self.assertEqual(conn.read(8), b'request')
|
self.assertEqual(conn.recv(8), b'request')
|
||||||
conn.write(b'response')
|
conn.sendall(b'response')
|
||||||
|
|
||||||
stage_1.wait()
|
stage_1.wait()
|
||||||
conn.sendall(b'x' * SENDALL_SIZE)
|
conn.sendall(b'x' * SENDALL_SIZE)
|
||||||
@@ -144,41 +143,73 @@ class SSLTest(LimitedTestCase):
|
|||||||
client_sock = eventlet.connect(server_sock.getsockname())
|
client_sock = eventlet.connect(server_sock.getsockname())
|
||||||
client_sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, BUFFER_SIZE)
|
client_sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, BUFFER_SIZE)
|
||||||
client = ssl.wrap_socket(client_sock)
|
client = ssl.wrap_socket(client_sock)
|
||||||
client.write(b'request')
|
client.sendall(b'request')
|
||||||
self.assertEqual(client.read(8), b'response')
|
self.assertEqual(client.recv(8), b'response')
|
||||||
stage_1.send()
|
stage_1.send()
|
||||||
|
|
||||||
check_idle_cpu_usage(0.2, 0.1)
|
tests.check_idle_cpu_usage(0.2, 0.1)
|
||||||
server_coro.kill()
|
server_coro.kill()
|
||||||
|
|
||||||
@skip_if_no_ssl
|
@tests.skip_if_no_ssl
|
||||||
def test_greensslobject(self):
|
def test_greensslobject(self):
|
||||||
def serve(listener):
|
def serve(listener):
|
||||||
sock, addr = listener.accept()
|
sock, addr = listener.accept()
|
||||||
sock.write(b'content')
|
sock.sendall(b'content')
|
||||||
greenio.shutdown_safe(sock)
|
greenio.shutdown_safe(sock)
|
||||||
sock.close()
|
sock.close()
|
||||||
listener = listen_ssl_socket(('', 0))
|
listener = listen_ssl_socket()
|
||||||
eventlet.spawn(serve, listener)
|
eventlet.spawn(serve, listener)
|
||||||
client = ssl.wrap_socket(
|
client = ssl.wrap_socket(eventlet.connect(listener.getsockname()))
|
||||||
eventlet.connect(('localhost', listener.getsockname()[1])))
|
self.assertEqual(client.recv(1024), b'content')
|
||||||
self.assertEqual(client.read(1024), b'content')
|
self.assertEqual(client.recv(1024), b'')
|
||||||
self.assertEqual(client.read(1024), b'')
|
|
||||||
|
|
||||||
@skip_if_no_ssl
|
@tests.skip_if_no_ssl
|
||||||
def test_regression_gh_17(self):
|
def test_regression_gh_17(self):
|
||||||
def serve(listener):
|
# https://github.com/eventlet/eventlet/issues/17
|
||||||
sock, addr = listener.accept()
|
# ssl wrapped but unconnected socket methods go special code path
|
||||||
|
# test that path at least for syntax/typo errors
|
||||||
|
sock = ssl.wrap_socket(socket.socket())
|
||||||
|
sock.settimeout(0.01)
|
||||||
|
try:
|
||||||
|
sock.sendall(b'')
|
||||||
|
except ssl.SSLError as e:
|
||||||
|
assert 'timed out' in str(e)
|
||||||
|
|
||||||
# to simulate condition mentioned in GH-17
|
@tests.skip_if_no_ssl
|
||||||
sock._sslobj = None
|
def test_no_handshake_block_accept_loop(self):
|
||||||
sock.sendall(b'some data')
|
listener = listen_ssl_socket()
|
||||||
greenio.shutdown_safe(sock)
|
listener.settimeout(0.3)
|
||||||
sock.close()
|
|
||||||
|
|
||||||
listener = listen_ssl_socket(('', 0))
|
def serve(sock):
|
||||||
eventlet.spawn(serve, listener)
|
try:
|
||||||
ssl.wrap_socket(eventlet.connect(('localhost', listener.getsockname()[1])))
|
name = sock.recv(8)
|
||||||
|
sock.sendall(b'hello ' + name)
|
||||||
|
except Exception:
|
||||||
|
# ignore evil clients
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
greenio.shutdown_safe(sock)
|
||||||
|
sock.close()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
def accept_loop():
|
||||||
main()
|
while True:
|
||||||
|
try:
|
||||||
|
sock, _ = listener.accept()
|
||||||
|
except socket.error:
|
||||||
|
return
|
||||||
|
eventlet.spawn(serve, sock)
|
||||||
|
|
||||||
|
loopt = eventlet.spawn(accept_loop)
|
||||||
|
|
||||||
|
# evil no handshake
|
||||||
|
evil = eventlet.connect(listener.getsockname())
|
||||||
|
good = ssl.wrap_socket(eventlet.connect(listener.getsockname()))
|
||||||
|
good.sendall(b'good')
|
||||||
|
response = good.recv(16)
|
||||||
|
good.close()
|
||||||
|
assert response == b'hello good'
|
||||||
|
evil.close()
|
||||||
|
|
||||||
|
listener.close()
|
||||||
|
loopt.wait()
|
||||||
|
eventlet.sleep(0)
|
||||||
|
Reference in New Issue
Block a user