mirror of
https://github.com/frappe/gunicorn.git
synced 2026-07-04 11:41:32 +08:00
gevent: Require gevent 23.9.0+ for security fixes
Address CVE-2023-41419 (Critical - remote privilege escalation via WSGIServer) by requiring gevent 23.9.0 or higher. Changes: - Update minimum gevent version from 1.4.0 to 23.9.0 - Remove legacy server.kill() code path (gevent < 1.0) - Update documentation to reflect new version requirement - Add comprehensive tests for gevent worker
This commit is contained in:
parent
4b9d787c93
commit
543854c123
@ -680,7 +680,7 @@ class WorkerClass(Setting):
|
||||
* ``sync``
|
||||
* ``eventlet`` - Requires eventlet >= 0.24.1 (or install it via
|
||||
``pip install gunicorn[eventlet]``)
|
||||
* ``gevent`` - Requires gevent >= 1.4 (or install it via
|
||||
* ``gevent`` - Requires gevent >= 23.9.0 (or install it via
|
||||
``pip install gunicorn[gevent]``)
|
||||
* ``tornado`` - Requires tornado >= 0.2 (or install it via
|
||||
``pip install gunicorn[tornado]``)
|
||||
|
||||
@ -11,11 +11,11 @@ import time
|
||||
try:
|
||||
import gevent
|
||||
except ImportError:
|
||||
raise RuntimeError("gevent worker requires gevent 1.4 or higher")
|
||||
raise RuntimeError("gevent worker requires gevent 23.9.0 or higher")
|
||||
else:
|
||||
from packaging.version import parse as parse_version
|
||||
if parse_version(gevent.__version__) < parse_version('1.4'):
|
||||
raise RuntimeError("gevent worker requires gevent 1.4 or higher")
|
||||
if parse_version(gevent.__version__) < parse_version('23.9.0'):
|
||||
raise RuntimeError("gevent worker requires gevent 23.9.0 or higher")
|
||||
|
||||
from gevent.pool import Pool
|
||||
from gevent.server import StreamServer
|
||||
@ -89,10 +89,7 @@ class GeventWorker(AsyncWorker):
|
||||
try:
|
||||
# Stop accepting requests
|
||||
for server in servers:
|
||||
if hasattr(server, 'close'): # gevent 1.0
|
||||
server.close()
|
||||
if hasattr(server, 'kill'): # gevent < 1.0
|
||||
server.kill()
|
||||
server.close()
|
||||
|
||||
# Handle current requests until graceful_timeout
|
||||
ts = time.time()
|
||||
|
||||
@ -47,7 +47,7 @@ Documentation = "https://docs.gunicorn.org"
|
||||
Changelog = "https://docs.gunicorn.org/en/stable/news.html"
|
||||
|
||||
[project.optional-dependencies]
|
||||
gevent = ["gevent>=1.4.0"]
|
||||
gevent = ["gevent>=23.9.0"]
|
||||
eventlet = ["eventlet>=0.24.1,!=0.36.0"]
|
||||
tornado = ["tornado>=6.5.0"]
|
||||
gthread = []
|
||||
|
||||
@ -2,5 +2,215 @@
|
||||
# This file is part of gunicorn released under the MIT license.
|
||||
# See the NOTICE for more information.
|
||||
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
try:
|
||||
import gevent
|
||||
HAS_GEVENT = True
|
||||
except ImportError:
|
||||
HAS_GEVENT = False
|
||||
|
||||
pytestmark = pytest.mark.skipif(not HAS_GEVENT, reason="gevent not installed")
|
||||
|
||||
|
||||
def test_import():
|
||||
__import__('gunicorn.workers.ggevent')
|
||||
|
||||
|
||||
def test_version_requirement():
|
||||
"""Test that gevent 23.9.0+ is required."""
|
||||
from gunicorn.workers import ggevent
|
||||
from packaging.version import parse as parse_version
|
||||
assert parse_version(gevent.__version__) >= parse_version('23.9.0')
|
||||
|
||||
|
||||
class TestGeventWorkerInit:
|
||||
"""Test GeventWorker initialization."""
|
||||
|
||||
def test_worker_has_no_server_class(self):
|
||||
"""Test that GeventWorker has no server_class by default."""
|
||||
from gunicorn.workers.ggevent import GeventWorker
|
||||
assert GeventWorker.server_class is None
|
||||
|
||||
def test_worker_has_no_wsgi_handler(self):
|
||||
"""Test that GeventWorker has no wsgi_handler by default."""
|
||||
from gunicorn.workers.ggevent import GeventWorker
|
||||
assert GeventWorker.wsgi_handler is None
|
||||
|
||||
def test_init_process_patches_and_reinits(self):
|
||||
"""Test that init_process calls patch and reinits the hub."""
|
||||
from gunicorn.workers.ggevent import GeventWorker
|
||||
|
||||
worker = mock.Mock(spec=GeventWorker)
|
||||
worker.sockets = []
|
||||
|
||||
with mock.patch('gunicorn.workers.ggevent.hub') as mock_hub, \
|
||||
mock.patch.object(GeventWorker.__bases__[0], 'init_process'):
|
||||
GeventWorker.init_process(worker)
|
||||
|
||||
# Verify patch was called
|
||||
worker.patch.assert_called_once()
|
||||
mock_hub.reinit.assert_called_once()
|
||||
|
||||
|
||||
class TestGeventWorkerRun:
|
||||
"""Test GeventWorker run method."""
|
||||
|
||||
def test_run_creates_stream_servers(self):
|
||||
"""Test that run creates StreamServer instances for each socket."""
|
||||
from gunicorn.workers.ggevent import GeventWorker
|
||||
|
||||
worker = mock.Mock(spec=GeventWorker)
|
||||
worker.sockets = [mock.Mock()]
|
||||
worker.cfg = mock.Mock(is_ssl=False, workers=1, graceful_timeout=30)
|
||||
worker.server_class = None
|
||||
worker.worker_connections = 1000
|
||||
|
||||
# Make alive return True once, then False to exit the loop
|
||||
worker.alive = False
|
||||
|
||||
with mock.patch('gunicorn.workers.ggevent.Pool') as mock_pool, \
|
||||
mock.patch('gunicorn.workers.ggevent.StreamServer') as mock_server_cls, \
|
||||
mock.patch('gunicorn.workers.ggevent.gevent') as mock_gevent:
|
||||
|
||||
mock_server = mock.Mock()
|
||||
mock_server.pool = mock.Mock()
|
||||
mock_server.pool.free_count.return_value = mock_server.pool.size
|
||||
mock_server_cls.return_value = mock_server
|
||||
|
||||
GeventWorker.run(worker)
|
||||
|
||||
mock_server_cls.assert_called_once()
|
||||
mock_server.start.assert_called_once()
|
||||
mock_server.close.assert_called_once()
|
||||
|
||||
def test_run_with_ssl(self):
|
||||
"""Test that run configures SSL context when is_ssl is True."""
|
||||
from gunicorn.workers.ggevent import GeventWorker
|
||||
|
||||
worker = mock.Mock(spec=GeventWorker)
|
||||
worker.sockets = [mock.Mock()]
|
||||
worker.cfg = mock.Mock(is_ssl=True, workers=1, graceful_timeout=30)
|
||||
worker.server_class = None
|
||||
worker.worker_connections = 1000
|
||||
worker.alive = False
|
||||
|
||||
with mock.patch('gunicorn.workers.ggevent.Pool'), \
|
||||
mock.patch('gunicorn.workers.ggevent.StreamServer') as mock_server_cls, \
|
||||
mock.patch('gunicorn.workers.ggevent.gevent'), \
|
||||
mock.patch('gunicorn.workers.ggevent.ssl_context') as mock_ssl_ctx:
|
||||
|
||||
mock_server = mock.Mock()
|
||||
mock_server.pool = mock.Mock()
|
||||
mock_server.pool.free_count.return_value = mock_server.pool.size
|
||||
mock_server_cls.return_value = mock_server
|
||||
mock_ssl_ctx.return_value = mock.Mock()
|
||||
|
||||
GeventWorker.run(worker)
|
||||
|
||||
mock_ssl_ctx.assert_called_once_with(worker.cfg)
|
||||
# Verify ssl_context was passed to StreamServer
|
||||
call_kwargs = mock_server_cls.call_args[1]
|
||||
assert 'ssl_context' in call_kwargs
|
||||
|
||||
|
||||
class TestSignalHandling:
|
||||
"""Test signal handling in GeventWorker."""
|
||||
|
||||
def test_handle_quit_spawns_greenlet(self):
|
||||
"""Test that handle_quit spawns a greenlet instead of blocking."""
|
||||
from gunicorn.workers.ggevent import GeventWorker
|
||||
|
||||
worker = mock.Mock(spec=GeventWorker)
|
||||
|
||||
with mock.patch('gunicorn.workers.ggevent.gevent') as mock_gevent:
|
||||
GeventWorker.handle_quit(worker, mock.Mock(), mock.Mock())
|
||||
mock_gevent.spawn.assert_called_once()
|
||||
|
||||
def test_handle_usr1_spawns_greenlet(self):
|
||||
"""Test that handle_usr1 spawns a greenlet instead of blocking."""
|
||||
from gunicorn.workers.ggevent import GeventWorker
|
||||
|
||||
worker = mock.Mock(spec=GeventWorker)
|
||||
|
||||
with mock.patch('gunicorn.workers.ggevent.gevent') as mock_gevent:
|
||||
GeventWorker.handle_usr1(worker, mock.Mock(), mock.Mock())
|
||||
mock_gevent.spawn.assert_called_once()
|
||||
|
||||
def test_notify_exits_on_parent_change(self):
|
||||
"""Test that notify exits when parent PID changes."""
|
||||
from gunicorn.workers.ggevent import GeventWorker
|
||||
|
||||
worker = mock.Mock(spec=GeventWorker)
|
||||
worker.ppid = 1234
|
||||
worker.log = mock.Mock()
|
||||
|
||||
with mock.patch('gunicorn.workers.ggevent.os') as mock_os, \
|
||||
mock.patch.object(GeventWorker.__bases__[0], 'notify'):
|
||||
mock_os.getppid.return_value = 5678 # Different PID
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
GeventWorker.notify(worker)
|
||||
|
||||
|
||||
class TestPyWSGIWorker:
|
||||
"""Test PyWSGI-based worker classes."""
|
||||
|
||||
def test_pywsgi_worker_has_server_class(self):
|
||||
"""Test that GeventPyWSGIWorker has proper server_class."""
|
||||
from gunicorn.workers.ggevent import GeventPyWSGIWorker, PyWSGIServer
|
||||
assert GeventPyWSGIWorker.server_class is PyWSGIServer
|
||||
|
||||
def test_pywsgi_worker_has_handler(self):
|
||||
"""Test that GeventPyWSGIWorker has proper wsgi_handler."""
|
||||
from gunicorn.workers.ggevent import GeventPyWSGIWorker, PyWSGIHandler
|
||||
assert GeventPyWSGIWorker.wsgi_handler is PyWSGIHandler
|
||||
|
||||
def test_pywsgi_handler_get_environ(self):
|
||||
"""Test that PyWSGIHandler adds gunicorn-specific environ keys."""
|
||||
from gunicorn.workers.ggevent import PyWSGIHandler
|
||||
|
||||
handler = mock.Mock(spec=PyWSGIHandler)
|
||||
handler.socket = mock.Mock()
|
||||
handler.path = '/test/path'
|
||||
|
||||
# Mock the parent get_environ
|
||||
with mock.patch.object(PyWSGIHandler.__bases__[0], 'get_environ', return_value={}):
|
||||
env = PyWSGIHandler.get_environ(handler)
|
||||
assert env['gunicorn.sock'] == handler.socket
|
||||
assert env['RAW_URI'] == '/test/path'
|
||||
|
||||
|
||||
class TestGeventResponse:
|
||||
"""Test GeventResponse helper class."""
|
||||
|
||||
def test_response_attributes(self):
|
||||
"""Test GeventResponse stores status, headers, and sent."""
|
||||
from gunicorn.workers.ggevent import GeventResponse
|
||||
|
||||
resp = GeventResponse('200 OK', {'Content-Type': 'text/html'}, 1024)
|
||||
assert resp.status == '200 OK'
|
||||
assert resp.headers == {'Content-Type': 'text/html'}
|
||||
assert resp.sent == 1024
|
||||
|
||||
|
||||
class TestTimeoutContext:
|
||||
"""Test timeout context manager."""
|
||||
|
||||
def test_timeout_ctx_uses_keepalive(self):
|
||||
"""Test that timeout_ctx uses cfg.keepalive."""
|
||||
from gunicorn.workers.ggevent import GeventWorker
|
||||
|
||||
worker = mock.Mock(spec=GeventWorker)
|
||||
worker.cfg = mock.Mock(keepalive=30)
|
||||
|
||||
with mock.patch('gunicorn.workers.ggevent.gevent') as mock_gevent:
|
||||
mock_timeout = mock.Mock()
|
||||
mock_gevent.Timeout.return_value = mock_timeout
|
||||
|
||||
result = GeventWorker.timeout_ctx(worker)
|
||||
|
||||
mock_gevent.Timeout.assert_called_once_with(30, False)
|
||||
assert result == mock_timeout
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user