Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-waitress for openSUSE:Factory checked in at 2024-10-31 16:08:55 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-waitress (Old) and /work/SRC/openSUSE:Factory/.python-waitress.new.2020 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Package is "python-waitress" Thu Oct 31 16:08:55 2024 rev:33 rq:1219322 version:3.0.1 Changes: -------- --- /work/SRC/openSUSE:Factory/python-waitress/python-waitress.changes 2024-07-03 20:29:28.463251390 +0200 +++ /work/SRC/openSUSE:Factory/.python-waitress.new.2020/python-waitress.changes 2024-10-31 16:09:02.266238438 +0100 @@ -1,0 +2,18 @@ +Wed Oct 30 06:49:46 UTC 2024 - Daniel Garcia <daniel.garcia@suse.com> + +- Update to 3.0.1 (bsc#1232554, bsc#1232556, CVE-2024-49769, CVE-2024-49768): + * Fix a bug that would lead to Waitress busy looping on select() + on a half-open socket due to a race condition that existed when + creating a new HTTPChannel. See + https://github.com/Pylons/waitress/pull/435, + https://github.com/Pylons/waitress/issues/418 and + https://github.com/Pylons/waitress/security/advisories/GHSA-3f84-rpwh-47g6 + * No longer strip the header values before passing them to the + WSGI environ. See https://github.com/Pylons/waitress/pull/434 + and https://github.com/Pylons/waitress/issues/432 + * Fix a race condition in Waitress when + `channel_request_lookahead` is enabled that could lead to HTTP + request smuggling. + * See https://github.com/Pylons/waitress/security/advisories/GHSA-9298-4cf8-g4wj + +------------------------------------------------------------------- Old: ---- waitress-3.0.0.tar.gz New: ---- waitress-3.0.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-waitress.spec ++++++ --- /var/tmp/diff_new_pack.TI4UFQ/_old 2024-10-31 16:09:03.818303200 +0100 +++ /var/tmp/diff_new_pack.TI4UFQ/_new 2024-10-31 16:09:03.830303701 +0100 @@ -31,7 +31,7 @@ %endif %{?sle15_python_module_pythons} Name: python-waitress%{psuffix} -Version: 3.0.0 +Version: 3.0.1 Release: 0 Summary: Waitress WSGI server License: ZPL-2.1 ++++++ waitress-3.0.0.tar.gz -> waitress-3.0.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/waitress-3.0.0/.github/workflows/ci-tests.yml new/waitress-3.0.1/.github/workflows/ci-tests.yml --- old/waitress-3.0.0/.github/workflows/ci-tests.yml 2024-02-04 23:39:05.000000000 +0100 +++ new/waitress-3.0.1/.github/workflows/ci-tests.yml 2024-10-27 02:15:47.000000000 +0100 @@ -7,6 +7,7 @@ - main - "[0-9].[0-9]+-branch" tags: + - "*" # Build pull requests pull_request: @@ -15,44 +16,62 @@ strategy: matrix: py: - - "3.8" - "3.9" - "3.10" - "3.11" - "3.12" - - "pypy-3.8" + - "3.13" - "pypy-3.9" - "pypy-3.10" # Pre-release os: - - "ubuntu-latest" + - "ubuntu-22.04" - "windows-latest" - - "macos-latest" + - "macos-14" # arm64 + - "macos-13" # x64 architecture: - x64 - x86 + - arm64 include: - - py: "pypy-3.8" - toxenv: "pypy38" - - py: "pypy-3.9" - toxenv: "pypy39" - - py: "pypy-3.10" - toxenv: "pypy310" + - py: "pypy-3.9" + toxenv: "pypy39" + - py: "pypy-3.10" + toxenv: "pypy310" exclude: - # Linux and macOS don't have x86 python - - os: "ubuntu-latest" + # Ubuntu does not have x86/arm64 Python + - os: "ubuntu-22.04" architecture: x86 - - os: "macos-latest" + - os: "ubuntu-22.04" + architecture: arm64 + # MacOS we need to make sure to remove x86 on all + # We need to run no arm64 on macos-13 (Intel), but some + # Python versions: 3.9/3.10 + # + # From 3.11 onward, there is support for running x64 and + # arm64 on Apple Silicon based systems (macos-14) + - os: "macos-13" architecture: x86 + - os: "macos-13" + architecture: arm64 + - os: "macos-14" + architecture: x86 + - os: "macos-14" + architecture: x64 + py: "3.9" + - os: "macos-14" + architecture: x64 + py: "3.10" + # Windows does not have arm64 releases + - os: "windows-latest" + architecture: arm64 # Don't run all PyPy versions except latest on # Windows/macOS. They are expensive to run. - os: "windows-latest" - py: "pypy-3.8" - - os: "macos-latest" - py: "pypy-3.8" - - os: "windows-latest" py: "pypy-3.9" - - os: "macos-latest" + - os: "macos-13" + py: "pypy-3.9" + - os: "macos-14" py: "pypy-3.9" name: "Python: ${{ matrix.py }}-${{ matrix.architecture }} on ${{ matrix.os }}" @@ -75,39 +94,39 @@ run: tox -e py coverage: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 name: Validate coverage steps: - uses: actions/checkout@v4 - - name: Setup python 3.10 + - name: Setup python uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.13" architecture: x64 - run: pip install tox - - run: tox -e py310,coverage + - run: tox -e py313,coverage docs: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 name: Build the documentation steps: - uses: actions/checkout@v4 - name: Setup python uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.13" architecture: x64 - run: pip install tox - run: tox -e docs lint: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 name: Lint the package steps: - uses: actions/checkout@v4 - name: Setup python uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.13" architecture: x64 - run: pip install tox - run: tox -e lint diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/waitress-3.0.0/.readthedocs.yaml new/waitress-3.0.1/.readthedocs.yaml --- old/waitress-3.0.0/.readthedocs.yaml 1970-01-01 01:00:00.000000000 +0100 +++ new/waitress-3.0.1/.readthedocs.yaml 2024-06-08 23:51:37.000000000 +0200 @@ -0,0 +1,17 @@ +# https://docs.readthedocs.io/en/stable/config-file/v2.html +version: 2 +build: + os: ubuntu-22.04 + tools: + python: '3.12' +sphinx: + configuration: docs/conf.py +formats: + - pdf + - epub +python: + install: + - method: pip + path: . + extra_requirements: + - docs diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/waitress-3.0.0/CHANGES.txt new/waitress-3.0.1/CHANGES.txt --- old/waitress-3.0.0/CHANGES.txt 2024-02-05 00:30:04.000000000 +0100 +++ new/waitress-3.0.1/CHANGES.txt 2024-10-29 01:09:00.000000000 +0100 @@ -1,3 +1,27 @@ +3.0.1 (2024-11-28) +------------------ + +Security +~~~~~~~~ + +- Fix a bug that would lead to Waitress busy looping on select() on a half-open + socket due to a race condition that existed when creating a new HTTPChannel. + See https://github.com/Pylons/waitress/pull/435, + https://github.com/Pylons/waitress/issues/418 and + https://github.com/Pylons/waitress/security/advisories/GHSA-3f84-rpwh-47g6 + + With thanks to Dylan Jay and Dieter Maurer for their extensive debugging and + helping track this down. + +- No longer strip the header values before passing them to the WSGI environ. + See https://github.com/Pylons/waitress/pull/434 and + https://github.com/Pylons/waitress/issues/432 + +- Fix a race condition in Waitress when `channel_request_lookahead` is enabled + that could lead to HTTP request smuggling. + + See https://github.com/Pylons/waitress/security/advisories/GHSA-9298-4cf8-g4wj + 3.0.0 (2024-02-04) ------------------ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/waitress-3.0.0/MANIFEST.in new/waitress-3.0.1/MANIFEST.in --- old/waitress-3.0.0/MANIFEST.in 2022-01-14 03:57:32.000000000 +0100 +++ new/waitress-3.0.1/MANIFEST.in 2024-06-09 00:10:10.000000000 +0200 @@ -14,7 +14,7 @@ include pyproject.toml setup.cfg include .coveragerc .flake8 -include tox.ini rtd.txt +include tox.ini .readthedocs.yaml exclude TODO.txt prune docs/_build diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/waitress-3.0.0/PKG-INFO new/waitress-3.0.1/PKG-INFO --- old/waitress-3.0.0/PKG-INFO 2024-02-05 00:32:02.214200300 +0100 +++ new/waitress-3.0.1/PKG-INFO 2024-10-29 01:11:17.053294000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: waitress -Version: 3.0.0 +Version: 3.0.1 Summary: Waitress WSGI server Home-page: https://github.com/Pylons/waitress Author: Zope Foundation and Contributors @@ -18,17 +18,17 @@ Classifier: License :: OSI Approved :: Zope Public License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Operating System :: OS Independent Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Internet :: WWW/HTTP :: WSGI -Requires-Python: >=3.8.0 +Requires-Python: >=3.9.0 Description-Content-Type: text/x-rst License-File: LICENSE.txt Provides-Extra: testing @@ -50,19 +50,43 @@ .. image:: https://github.com/Pylons/waitress/actions/workflows/ci-tests.yml/badge.svg :target: https://github.com/Pylons/waitress/actions/workflows/ci-tests.yml -.. image:: https://readthedocs.org/projects/waitress/badge/?version=main - :target: https://docs.pylonsproject.org/projects/waitress/en/main +.. image:: https://readthedocs.org/projects/waitress/badge/?version=stable + :target: https://docs.pylonsproject.org/projects/waitress/en/stable/ :alt: main Documentation Status Waitress is a production-quality pure-Python WSGI server with very acceptable performance. It has no dependencies except ones which live in the Python -standard library. It runs on CPython on Unix and Windows under Python 3.8+. It -is also known to run on PyPy 3 (version 3.8 compatible python and above) on +standard library. It runs on CPython on Unix and Windows under Python 3.9+. It +is also known to run on PyPy 3 (version 3.9 compatible python and above) on UNIX. It supports HTTP/1.0 and HTTP/1.1. For more information, see the "docs" directory of the Waitress package or visit https://docs.pylonsproject.org/projects/waitress/en/latest/ +3.0.1 (2024-11-28) +------------------ + +Security +~~~~~~~~ + +- Fix a bug that would lead to Waitress busy looping on select() on a half-open + socket due to a race condition that existed when creating a new HTTPChannel. + See https://github.com/Pylons/waitress/pull/435, + https://github.com/Pylons/waitress/issues/418 and + https://github.com/Pylons/waitress/security/advisories/GHSA-3f84-rpwh-47g6 + + With thanks to Dylan Jay and Dieter Maurer for their extensive debugging and + helping track this down. + +- No longer strip the header values before passing them to the WSGI environ. + See https://github.com/Pylons/waitress/pull/434 and + https://github.com/Pylons/waitress/issues/432 + +- Fix a race condition in Waitress when `channel_request_lookahead` is enabled + that could lead to HTTP request smuggling. + + See https://github.com/Pylons/waitress/security/advisories/GHSA-9298-4cf8-g4wj + 3.0.0 (2024-02-04) ------------------ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/waitress-3.0.0/README.rst new/waitress-3.0.1/README.rst --- old/waitress-3.0.0/README.rst 2024-02-04 23:39:05.000000000 +0100 +++ new/waitress-3.0.1/README.rst 2024-10-27 02:15:47.000000000 +0100 @@ -8,14 +8,14 @@ .. image:: https://github.com/Pylons/waitress/actions/workflows/ci-tests.yml/badge.svg :target: https://github.com/Pylons/waitress/actions/workflows/ci-tests.yml -.. image:: https://readthedocs.org/projects/waitress/badge/?version=main - :target: https://docs.pylonsproject.org/projects/waitress/en/main +.. image:: https://readthedocs.org/projects/waitress/badge/?version=stable + :target: https://docs.pylonsproject.org/projects/waitress/en/stable/ :alt: main Documentation Status Waitress is a production-quality pure-Python WSGI server with very acceptable performance. It has no dependencies except ones which live in the Python -standard library. It runs on CPython on Unix and Windows under Python 3.8+. It -is also known to run on PyPy 3 (version 3.8 compatible python and above) on +standard library. It runs on CPython on Unix and Windows under Python 3.9+. It +is also known to run on PyPy 3 (version 3.9 compatible python and above) on UNIX. It supports HTTP/1.0 and HTTP/1.1. For more information, see the "docs" directory of the Waitress package or visit diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/waitress-3.0.0/docs/arguments.rst new/waitress-3.0.1/docs/arguments.rst --- old/waitress-3.0.0/docs/arguments.rst 2024-02-05 00:09:10.000000000 +0100 +++ new/waitress-3.0.1/docs/arguments.rst 2024-10-29 01:06:11.000000000 +0100 @@ -314,3 +314,17 @@ be stripped of the prefix. Default: ``''`` + +channel_request_lookahead + Sets the amount of requests we can continue to read from the socket, while + we are processing current requests. The default value won't allow any + lookahead, increase it above ``0`` to enable. + + When enabled this inserts a callable ``waitress.client_disconnected`` into + the environment that allows the task to check if the client disconnected + while waiting for the response at strategic points in the execution and to + cancel the operation. + + Default: ``0`` + + .. versionadded:: 2.0.0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/waitress-3.0.0/rtd.txt new/waitress-3.0.1/rtd.txt --- old/waitress-3.0.0/rtd.txt 2017-09-15 22:45:36.000000000 +0200 +++ new/waitress-3.0.1/rtd.txt 1970-01-01 01:00:00.000000000 +0100 @@ -1,3 +0,0 @@ -Sphinx >= 1.3.1 -repoze.sphinx.autointerface -pylons-sphinx-themes diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/waitress-3.0.0/setup.cfg new/waitress-3.0.1/setup.cfg --- old/waitress-3.0.0/setup.cfg 2024-02-05 00:32:02.214689500 +0100 +++ new/waitress-3.0.1/setup.cfg 2024-10-29 01:11:17.053846600 +0100 @@ -1,6 +1,6 @@ [metadata] name = waitress -version = 3.0.0 +version = 3.0.1 description = Waitress WSGI server long_description = file: README.rst, CHANGES.txt long_description_content_type = text/x-rst @@ -13,11 +13,11 @@ License :: OSI Approved :: Zope Public License Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 + Programming Language :: Python :: 3.13 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy Operating System :: OS Independent @@ -37,7 +37,7 @@ package_dir = =src packages = find: -python_requires = >=3.8.0 +python_requires = >=3.9.0 [options.entry_points] paste.server_runner = diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/waitress-3.0.0/src/waitress/channel.py new/waitress-3.0.1/src/waitress/channel.py --- old/waitress-3.0.0/src/waitress/channel.py 2022-12-24 22:07:01.000000000 +0100 +++ new/waitress-3.0.1/src/waitress/channel.py 2024-10-29 01:06:11.000000000 +0100 @@ -67,8 +67,7 @@ self.outbuf_lock = threading.Condition() wasyncore.dispatcher.__init__(self, sock, map=map) - - # Don't let wasyncore.dispatcher throttle self.addr on us. + self.connected = True self.addr = addr self.requests = [] @@ -92,13 +91,7 @@ # Precondition: there's data in the out buffer to be sent, or # there's a pending will_close request - if not self.connected: - # we dont want to close the channel twice - - return - # try to flush any pending output - if not self.requests: # 1. There are no running tasks, so we don't need to try to lock # the outbuf before sending @@ -147,7 +140,7 @@ # 1. We're not already about to close the connection. # 2. We're not waiting to flush remaining data before closing the # connection - # 3. There are not too many tasks already queued + # 3. There are not too many tasks already queued (if lookahead is enabled) # 4. There's no data in the output buffer that needs to be sent # before we potentially create a new task. @@ -203,6 +196,15 @@ return False with self.requests_lock: + # Don't bother processing anymore data if this connection is about + # to close. This may happen if readable() returned True, on the + # main thread before the service thread set the close_when_flushed + # flag, and we read data but our service thread is attempting to + # shut down the connection due to an error. We want to make sure we + # do this while holding the request_lock so that we can't race + if self.will_close or self.close_when_flushed: + return False + while data: if self.request is None: self.request = self.parser_class(self.adj) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/waitress-3.0.0/src/waitress/task.py new/waitress-3.0.1/src/waitress/task.py --- old/waitress-3.0.0/src/waitress/task.py 2024-02-04 23:52:18.000000000 +0100 +++ new/waitress-3.0.1/src/waitress/task.py 2024-03-03 22:56:33.000000000 +0100 @@ -557,7 +557,6 @@ } for key, value in dict(request.headers).items(): - value = value.strip() mykey = rename_headers.get(key, None) if mykey is None: mykey = "HTTP_" + key diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/waitress-3.0.0/src/waitress/wasyncore.py new/waitress-3.0.1/src/waitress/wasyncore.py --- old/waitress-3.0.0/src/waitress/wasyncore.py 2024-02-04 23:52:18.000000000 +0100 +++ new/waitress-3.0.1/src/waitress/wasyncore.py 2024-10-27 02:34:09.000000000 +0100 @@ -297,22 +297,6 @@ # get a socket from a blocking source. sock.setblocking(0) self.set_socket(sock, map) - self.connected = True - # The constructor no longer requires that the socket - # passed be connected. - try: - self.addr = sock.getpeername() - except OSError as err: - if err.args[0] in (ENOTCONN, EINVAL): - # To handle the case where we got an unconnected - # socket. - self.connected = False - else: - # The socket is broken in some unknown way, alert - # the user and remove it from the map (to prevent - # polling of broken sockets). - self.del_channel(map) - raise else: self.socket = None @@ -394,23 +378,6 @@ self.addr = addr return self.socket.bind(addr) - def connect(self, address): - self.connected = False - self.connecting = True - err = self.socket.connect_ex(address) - if ( - err in (EINPROGRESS, EALREADY, EWOULDBLOCK) - or err == EINVAL - and os.name == "nt" - ): # pragma: no cover - self.addr = address - return - if err in (0, EISCONN): - self.addr = address - self.handle_connect_event() - else: - raise OSError(err, errorcode[err]) - def accept(self): # XXX can return either an address pair or None try: @@ -469,6 +436,8 @@ if why.args[0] not in (ENOTCONN, EBADF): raise + self.socket = None + # log and log_info may be overridden to provide more sophisticated # logging and warning methods. In general, log is for 'hit' logging # and 'log_info' is for informational, warning and error logging. @@ -519,7 +488,11 @@ # handle_expt_event() is called if there might be an error on the # socket, or if there is OOB data # check for the error condition first - err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) + err = ( + self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) + if self.socket is not None + else 1 + ) if err != 0: # we can get here when select.select() says that there is an # exceptional condition on the socket @@ -572,34 +545,6 @@ self.close() -# --------------------------------------------------------------------------- -# adds simple buffered output capability, useful for simple clients. -# [for more sophisticated usage use asynchat.async_chat] -# --------------------------------------------------------------------------- - - -class dispatcher_with_send(dispatcher): - def __init__(self, sock=None, map=None): - dispatcher.__init__(self, sock, map) - self.out_buffer = b"" - - def initiate_send(self): - num_sent = 0 - num_sent = dispatcher.send(self, self.out_buffer[:65536]) - self.out_buffer = self.out_buffer[num_sent:] - - handle_write = initiate_send - - def writable(self): - return (not self.connected) or len(self.out_buffer) - - def send(self, data): - if self.debug: # pragma: no cover - self.log_info("sending %s" % repr(data)) - self.out_buffer = self.out_buffer + data - self.initiate_send() - - def close_all(map=None, ignore_all=False): if map is None: # pragma: no cover map = socket_map diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/waitress-3.0.0/src/waitress.egg-info/PKG-INFO new/waitress-3.0.1/src/waitress.egg-info/PKG-INFO --- old/waitress-3.0.0/src/waitress.egg-info/PKG-INFO 2024-02-05 00:32:02.000000000 +0100 +++ new/waitress-3.0.1/src/waitress.egg-info/PKG-INFO 2024-10-29 01:11:17.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: waitress -Version: 3.0.0 +Version: 3.0.1 Summary: Waitress WSGI server Home-page: https://github.com/Pylons/waitress Author: Zope Foundation and Contributors @@ -18,17 +18,17 @@ Classifier: License :: OSI Approved :: Zope Public License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Operating System :: OS Independent Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Internet :: WWW/HTTP :: WSGI -Requires-Python: >=3.8.0 +Requires-Python: >=3.9.0 Description-Content-Type: text/x-rst License-File: LICENSE.txt Provides-Extra: testing @@ -50,19 +50,43 @@ .. image:: https://github.com/Pylons/waitress/actions/workflows/ci-tests.yml/badge.svg :target: https://github.com/Pylons/waitress/actions/workflows/ci-tests.yml -.. image:: https://readthedocs.org/projects/waitress/badge/?version=main - :target: https://docs.pylonsproject.org/projects/waitress/en/main +.. image:: https://readthedocs.org/projects/waitress/badge/?version=stable + :target: https://docs.pylonsproject.org/projects/waitress/en/stable/ :alt: main Documentation Status Waitress is a production-quality pure-Python WSGI server with very acceptable performance. It has no dependencies except ones which live in the Python -standard library. It runs on CPython on Unix and Windows under Python 3.8+. It -is also known to run on PyPy 3 (version 3.8 compatible python and above) on +standard library. It runs on CPython on Unix and Windows under Python 3.9+. It +is also known to run on PyPy 3 (version 3.9 compatible python and above) on UNIX. It supports HTTP/1.0 and HTTP/1.1. For more information, see the "docs" directory of the Waitress package or visit https://docs.pylonsproject.org/projects/waitress/en/latest/ +3.0.1 (2024-11-28) +------------------ + +Security +~~~~~~~~ + +- Fix a bug that would lead to Waitress busy looping on select() on a half-open + socket due to a race condition that existed when creating a new HTTPChannel. + See https://github.com/Pylons/waitress/pull/435, + https://github.com/Pylons/waitress/issues/418 and + https://github.com/Pylons/waitress/security/advisories/GHSA-3f84-rpwh-47g6 + + With thanks to Dylan Jay and Dieter Maurer for their extensive debugging and + helping track this down. + +- No longer strip the header values before passing them to the WSGI environ. + See https://github.com/Pylons/waitress/pull/434 and + https://github.com/Pylons/waitress/issues/432 + +- Fix a race condition in Waitress when `channel_request_lookahead` is enabled + that could lead to HTTP request smuggling. + + See https://github.com/Pylons/waitress/security/advisories/GHSA-9298-4cf8-g4wj + 3.0.0 (2024-02-04) ------------------ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/waitress-3.0.0/src/waitress.egg-info/SOURCES.txt new/waitress-3.0.1/src/waitress.egg-info/SOURCES.txt --- old/waitress-3.0.0/src/waitress.egg-info/SOURCES.txt 2024-02-05 00:32:02.000000000 +0100 +++ new/waitress-3.0.1/src/waitress.egg-info/SOURCES.txt 2024-10-29 01:11:17.000000000 +0100 @@ -1,5 +1,6 @@ .coveragerc .flake8 +.readthedocs.yaml CHANGES.txt CONTRIBUTORS.txt COPYRIGHT.txt @@ -10,7 +11,6 @@ RELEASING.txt contributing.md pyproject.toml -rtd.txt setup.cfg setup.py tox.ini diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/waitress-3.0.0/tests/test_channel.py new/waitress-3.0.1/tests/test_channel.py --- old/waitress-3.0.0/tests/test_channel.py 2024-02-04 23:52:18.000000000 +0100 +++ new/waitress-3.0.1/tests/test_channel.py 2024-10-29 01:06:11.000000000 +0100 @@ -18,7 +18,7 @@ map = {} inst = self._makeOne(sock, "127.0.0.1", adj, map=map) inst.outbuf_lock = DummyLock() - return inst, sock, map + return inst, sock.local(), map def test_ctor(self): inst, _, map = self._makeOneWithMap() @@ -218,7 +218,7 @@ def send(_): return 0 - sock.send = send + sock.remote.send = send wrote = inst.write_soon(b"a") self.assertEqual(wrote, 1) @@ -236,7 +236,7 @@ def send(_): return 0 - sock.send = send + sock.remote.send = send outbufs = inst.outbufs wrote = inst.write_soon(wrapper) @@ -270,7 +270,7 @@ def send(_): return 0 - sock.send = send + sock.remote.send = send inst.adj.outbuf_high_watermark = 3 inst.current_outbuf_count = 4 @@ -286,7 +286,7 @@ def send(_): return 0 - sock.send = send + sock.remote.send = send inst.adj.outbuf_high_watermark = 3 inst.total_outbufs_len = 4 @@ -315,7 +315,7 @@ inst.connected = False raise Exception() - sock.send = send + sock.remote.send = send inst.adj.outbuf_high_watermark = 3 inst.total_outbufs_len = 4 @@ -345,7 +345,7 @@ inst.connected = False raise Exception() - sock.send = send + sock.remote.send = send wrote = inst.write_soon(b"xyz") self.assertEqual(wrote, 3) @@ -376,7 +376,7 @@ inst.total_outbufs_len = len(inst.outbufs[0]) inst.adj.send_bytes = 1 inst.adj.outbuf_high_watermark = 2 - sock.send = lambda x, do_close=True: False + sock.remote.send = lambda x, do_close=True: False inst.will_close = False inst.last_activity = 0 result = inst.handle_write() @@ -400,7 +400,7 @@ def test__flush_some_full_outbuf_socket_returns_zero(self): inst, sock, map = self._makeOneWithMap() - sock.send = lambda x: False + sock.remote.send = lambda x: False inst.outbufs[0].append(b"abc") inst.total_outbufs_len = sum(len(x) for x in inst.outbufs) result = inst._flush_some() @@ -805,11 +805,12 @@ ) return [body] - def _make_app_with_lookahead(self): + def _make_app_with_lookahead(self, recv_bytes=8192): """ Setup a channel with lookahead and store it and the socket in self """ adj = DummyAdjustments() + adj.recv_bytes = recv_bytes adj.channel_request_lookahead = 5 channel, sock, map = self._makeOneWithMap(adj=adj) channel.server.application = self.app_check_disconnect @@ -901,13 +902,66 @@ self.assertEqual(data.split("\r\n")[-1], "finished") self.assertEqual(self.request_body, b"x") + def test_lookahead_bad_request_drop_extra_data(self): + """ + Send two requests, the first one being bad, split on the recv_bytes + limit, then emulate a race that could happen whereby we read data from + the socket while the service thread is cleaning up due to an error + processing the request. + """ + + invalid_request = [ + "GET / HTTP/1.1", + "Host: localhost:8080", + "Content-length: -1", + "", + ] + + invalid_request_len = len("".join([x + "\r\n" for x in invalid_request])) + + second_request = [ + "POST / HTTP/1.1", + "Host: localhost:8080", + "Content-Length: 1", + "", + "x", + ] + + full_request = invalid_request + second_request + + self._make_app_with_lookahead(recv_bytes=invalid_request_len) + self._send(*full_request) + self.channel.handle_read() + self.assertEqual(len(self.channel.requests), 1) + self.channel.server.tasks[0].service() + self.assertTrue(self.channel.close_when_flushed) + # Read all of the next request + self.channel.handle_read() + self.channel.handle_read() + # Validate that there is no more data to be read + self.assertEqual(self.sock.remote.local_sent, b"") + # Validate that we dropped the data from the second read, and did not + # create a new request + self.assertEqual(len(self.channel.requests), 0) + data = self.sock.recv(256).decode("ascii") + self.assertFalse(self.channel.readable()) + self.assertTrue(self.channel.writable()) + + # Handle the write, which will close the socket + self.channel.handle_write() + self.assertTrue(self.sock.closed) + + data = self.sock.recv(256) + self.assertEqual(len(data), 0) + class DummySock: blocking = False closed = False def __init__(self): - self.sent = b"" + self.local_sent = b"" + self.remote_sent = b"" def setblocking(self, *arg): self.blocking = True @@ -925,14 +979,44 @@ self.closed = True def send(self, data): - self.sent += data + self.remote_sent += data return len(data) def recv(self, buffer_size): - result = self.sent[:buffer_size] - self.sent = self.sent[buffer_size:] + result = self.local_sent[:buffer_size] + self.local_sent = self.local_sent[buffer_size:] return result + def local(self): + outer = self + + class LocalDummySock: + def send(self, data): + outer.local_sent += data + return len(data) + + def recv(self, buffer_size): + result = outer.remote_sent[:buffer_size] + outer.remote_sent = outer.remote_sent[buffer_size:] + return result + + def close(self): + outer.closed = True + + @property + def sent(self): + return outer.remote_sent + + @property + def closed(self): + return outer.closed + + @property + def remote(self): + return outer + + return LocalDummySock() + class DummyLock: notified = False diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/waitress-3.0.0/tests/test_parser.py new/waitress-3.0.1/tests/test_parser.py --- old/waitress-3.0.0/tests/test_parser.py 2024-02-05 00:29:06.000000000 +0100 +++ new/waitress-3.0.1/tests/test_parser.py 2024-03-03 22:56:33.000000000 +0100 @@ -384,6 +384,11 @@ else: # pragma: nocover self.assertTrue(False) + def test_parse_header_other_whitespace(self): + data = b"GET /foobar HTTP/1.1\r\nfoo: \xa0something\x85\r\n" + self.parser.parse_header(data) + self.assertEqual(self.parser.headers["FOO"], "\xa0something\x85") + def test_parse_header_empty(self): data = b"GET /foobar HTTP/1.1\r\nfoo: bar\r\nempty:\r\n" self.parser.parse_header(data) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/waitress-3.0.0/tests/test_task.py new/waitress-3.0.1/tests/test_task.py --- old/waitress-3.0.0/tests/test_task.py 2024-02-04 23:39:05.000000000 +0100 +++ new/waitress-3.0.1/tests/test_task.py 2024-03-03 22:56:33.000000000 +0100 @@ -776,7 +776,7 @@ request.headers = { "CONTENT_TYPE": "abc", "CONTENT_LENGTH": "10", - "X_FOO": "BAR", + "X_FOO": "\xa0BAR\x85", "CONNECTION": "close", } request.query = "abc" @@ -830,7 +830,8 @@ self.assertEqual(environ["REMOTE_PORT"], "39830") self.assertEqual(environ["CONTENT_TYPE"], "abc") self.assertEqual(environ["CONTENT_LENGTH"], "10") - self.assertEqual(environ["HTTP_X_FOO"], "BAR") + # Make sure we don't strip non RFC compliant whitespace + self.assertEqual(environ["HTTP_X_FOO"], "\xa0BAR\x85") self.assertEqual(environ["wsgi.version"], (1, 0)) self.assertEqual(environ["wsgi.url_scheme"], "http") self.assertEqual(environ["wsgi.errors"], sys.stderr) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/waitress-3.0.0/tests/test_wasyncore.py new/waitress-3.0.1/tests/test_wasyncore.py --- old/waitress-3.0.0/tests/test_wasyncore.py 2024-02-04 20:29:14.000000000 +0100 +++ new/waitress-3.0.1/tests/test_wasyncore.py 2024-06-08 23:51:25.000000000 +0200 @@ -1,6 +1,7 @@ import _thread as thread import contextlib import errno +from errno import EALREADY, EINPROGRESS, EINVAL, EISCONN, EWOULDBLOCK, errorcode import functools import gc from io import BytesIO @@ -641,62 +642,6 @@ self.assertTrue(err != "") -class dispatcherwithsend_noread(asyncore.dispatcher_with_send): # pragma: no cover - def readable(self): - return False - - def handle_connect(self): - pass - - -class DispatcherWithSendTests(unittest.TestCase): - def setUp(self): - pass - - def tearDown(self): - asyncore.close_all() - - @reap_threads - def test_send(self): - evt = threading.Event() - sock = socket.socket() - sock.settimeout(3) - port = bind_port(sock) - - cap = BytesIO() - args = (evt, cap, sock) - t = threading.Thread(target=capture_server, args=args) - t.start() - try: - # wait a little longer for the server to initialize (it sometimes - # refuses connections on slow machines without this wait) - time.sleep(0.2) - - data = b"Suppose there isn't a 16-ton weight?" - d = dispatcherwithsend_noread() - d.create_socket() - d.connect((HOST, port)) - - # give time for socket to connect - time.sleep(0.1) - - d.send(data) - d.send(data) - d.send(b"\n") - - n = 1000 - - while d.out_buffer and n > 0: # pragma: no cover - asyncore.poll() - n -= 1 - - evt.wait() - - self.assertEqual(cap.getvalue(), data * 2) - finally: - join_thread(t, timeout=TIMEOUT) - - @unittest.skipUnless( hasattr(asyncore, "file_wrapper"), "asyncore.file_wrapper required" ) @@ -839,6 +784,23 @@ self.create_socket(family) self.connect(address) + def connect(self, address): + self.connected = False + self.connecting = True + err = self.socket.connect_ex(address) + if ( + err in (EINPROGRESS, EALREADY, EWOULDBLOCK) + or err == EINVAL + and os.name == "nt" + ): # pragma: no cover + self.addr = address + return + if err in (0, EISCONN): + self.addr = address + self.handle_connect_event() + else: + raise OSError(err, errorcode[err]) + def handle_connect(self): pass @@ -1454,17 +1416,6 @@ return dispatcher(sock=sock, map=map) - def test_unexpected_getpeername_exc(self): - sock = dummysocket() - - def getpeername(): - raise OSError(errno.EBADF) - - map = {} - sock.getpeername = getpeername - self.assertRaises(socket.error, self._makeOne, sock=sock, map=map) - self.assertEqual(map, {}) - def test___repr__accepting(self): sock = dummysocket() map = {} @@ -1500,13 +1451,6 @@ inst.set_reuse_addr() self.assertTrue(sock.errored) - def test_connect_raise_socket_error(self): - sock = dummysocket() - map = {} - sock.connect_ex = lambda *arg: 1 - inst = self._makeOne(sock=sock, map=map) - self.assertRaises(socket.error, inst.connect, 0) - def test_accept_raise_TypeError(self): sock = dummysocket() map = {} @@ -1675,21 +1619,6 @@ self.assertTrue(sock.closed) -class Test_dispatcher_with_send(unittest.TestCase): - def _makeOne(self, sock=None, map=None): - from waitress.wasyncore import dispatcher_with_send - - return dispatcher_with_send(sock=sock, map=map) - - def test_writable(self): - sock = dummysocket() - map = {} - inst = self._makeOne(sock=sock, map=map) - inst.out_buffer = b"123" - inst.connected = True - self.assertTrue(inst.writable()) - - class Test_close_all(unittest.TestCase): def _callFUT(self, map=None, ignore_all=False): from waitress.wasyncore import close_all diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/waitress-3.0.0/tox.ini new/waitress-3.0.1/tox.ini --- old/waitress-3.0.0/tox.ini 2024-02-04 23:39:05.000000000 +0100 +++ new/waitress-3.0.1/tox.ini 2024-10-27 02:15:47.000000000 +0100 @@ -1,7 +1,7 @@ [tox] envlist = lint, - py38,py39,py310,py311,py312,pypy38,pypy39,pypy310 + py39,py310,py311,py312,py313,pypy39,pypy310 coverage, docs isolated_build = True @@ -10,7 +10,6 @@ commands = python --version python -mpytest \ - pypy38: --no-cov \ pypy39: --no-cov \ pypy310: --no-cov \ {posargs:}