Compare commits

...

10 Commits

Author SHA1 Message Date
openeuler-ci-bot
8b3c6c6500
!34 [sync] PR-32: Fix CVE-2024-49769
From: @openeuler-sync-bot 
Reviewed-by: @shinwell_hu 
Signed-off-by: @shinwell_hu
2024-10-31 07:35:12 +00:00
liningjie
a87f3e17ef Fix CVE-2024-49769
(cherry picked from commit 20c8c1edb54c8d51f19bfe2c1f0a3d70149879b5)
2024-10-31 09:37:34 +08:00
openeuler-ci-bot
0716df1e62
!26 [sync] PR-23: Fix CVE-2024-49768
From: @openeuler-sync-bot 
Reviewed-by: @cherry530 
Signed-off-by: @cherry530
2024-10-30 07:02:59 +00:00
liningjie
57ca6654bc Fix CVE-2024-49768
(cherry picked from commit bb9d437bd7b2248668f04c606a5ad8eecb08fe7e)
2024-10-30 14:11:22 +08:00
openeuler-ci-bot
55b620f973
!11 Fix cve-2022-24761
From: @shinwell_hu 
Reviewed-by: @myeuler 
Signed-off-by: @myeuler
2022-04-22 06:50:27 +00:00
Shinwell Hu
d779c48193 fix cve 2022-24761 2022-04-22 06:36:24 +00:00
openeuler-ci-bot
f31b3a9255
!6 解除python-waitress对python-nose的依赖
Merge pull request !6 from 吴磊磊/openEuler-22.03-LTS-Next
2022-01-11 02:32:13 +00:00
wu-leilei
50034c75d8 remove nose dependency 2022-01-11 09:53:51 +08:00
openeuler-ci-bot
0ee5a2c56c !5 版本升级到2.0.0
From: @suo-xiaocong
Reviewed-by: @shinwell_hu
Signed-off-by: @shinwell_hu
2021-07-26 08:11:47 +00:00
suo-xiaocong
afb07b94de update package to 2.0.0 2021-07-08 16:06:42 +08:00
6 changed files with 1040 additions and 4 deletions

225
CVE-2024-49768.patch Normal file
View File

@ -0,0 +1,225 @@
From 6943dcf556610ece2ff3cddb39e59a05ef110661 Mon Sep 17 00:00:00 2001
From: Delta Regeer <bertjw@regeer.org>
Date: Sat, 26 Oct 2024 22:10:36 -0600
Subject: [PATCH 1/4] Make DummySock() look more like an actual socket
Fix CVE-2024-49768.
---
docs/arguments.rst | 14 ++++++
src/waitress/channel.py | 11 ++++-
tests/test_channel.py | 98 +++++++++++++++++++++++++++++++++++++----
3 files changed, 114 insertions(+), 9 deletions(-)
diff --git a/docs/arguments.rst b/docs/arguments.rst
index f9b9310..20fdfd0 100644
--- a/docs/arguments.rst
+++ b/docs/arguments.rst
@@ -301,3 +301,17 @@ url_prefix
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
\ No newline at end of file
diff --git a/src/waitress/channel.py b/src/waitress/channel.py
index 296a16a..20d0bb1 100644
--- a/src/waitress/channel.py
+++ b/src/waitress/channel.py
@@ -139,7 +139,7 @@ class HTTPChannel(wasyncore.dispatcher):
# 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.
@@ -194,6 +194,15 @@ class HTTPChannel(wasyncore.dispatcher):
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 --git a/tests/test_channel.py b/tests/test_channel.py
index d86dbbe..bfe083a 100644
--- a/tests/test_channel.py
+++ b/tests/test_channel.py
@@ -18,7 +18,7 @@ class TestHTTPChannel(unittest.TestCase):
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()
@@ -303,7 +303,7 @@ class TestHTTPChannel(unittest.TestCase):
inst.total_outbufs_len = len(inst.outbufs[0])
inst.adj.send_bytes = 1
inst.adj.outbuf_high_watermark = 2
- sock.send = lambda x: False
+ sock.remote.send = lambda x: False
inst.will_close = False
inst.last_activity = 0
result = inst.handle_write()
@@ -327,7 +327,7 @@ class TestHTTPChannel(unittest.TestCase):
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()
@@ -732,11 +732,12 @@ class TestHTTPChannelLookahead(TestHTTPChannel):
)
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
@@ -828,13 +829,65 @@ class TestHTTPChannelLookahead(TestHTTPChannel):
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
@@ -852,14 +905,43 @@ class DummySock:
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
--
2.27.0

401
CVE-2024-49769.patch Normal file
View File

@ -0,0 +1,401 @@
From 03cc640fe7106902899f82115c26e37002bca7f1 Mon Sep 17 00:00:00 2001
From: Delta Regeer <bertjw@regeer.org>
Date: Sun, 3 Mar 2024 16:15:51 -0700
Subject: [PATCH 1/6] HTTPChannel is always created from accept, explicitly set
self.connected to True
---
src/waitress/channel.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/waitress/channel.py b/src/waitress/channel.py
index eb59dd3..ea019d3 100644
--- a/src/waitress/channel.py
+++ b/src/waitress/channel.py
@@ -67,8 +67,7 @@ class HTTPChannel(wasyncore.dispatcher):
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 = []
--
Gitee
From 840aebce1c4c1bfd9036f402c1f5d5a4d2f4a1c2 Mon Sep 17 00:00:00 2001
From: Delta Regeer <bertjw@regeer.org>
Date: Sun, 3 Mar 2024 16:16:48 -0700
Subject: [PATCH 2/6] Assume socket is not connected when passed to
wasyncore.dispatcher
No longer call getpeername() on the remote socket either, as it is not
necessary for any of the places where waitress requires that self.addr
in a subclass of the dispatcher needs it.
This removes a race condition when setting up a HTTPChannel where we
accepted the socket, and know the remote address, yet call getpeername()
again which would have the unintended side effect of potentially setting
self.connected to False because the remote has already shut down part of
the socket.
This issue was uncovered in #418, where the server would go into a hard
loop because self.connected was used in various parts of the code base.
---
src/waitress/wasyncore.py | 16 ----------------
1 file changed, 16 deletions(-)
diff --git a/src/waitress/wasyncore.py b/src/waitress/wasyncore.py
index 8615557..9794308 100644
--- a/src/waitress/wasyncore.py
+++ b/src/waitress/wasyncore.py
@@ -297,22 +297,6 @@ class dispatcher:
# 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
--
Gitee
From 86c680df4e4bdd40c78dec771cddcee059e802c4 Mon Sep 17 00:00:00 2001
From: Delta Regeer <bertjw@regeer.org>
Date: Sun, 3 Mar 2024 16:23:33 -0700
Subject: [PATCH 3/6] Remove test for getpeername()
---
tests/test_wasyncore.py | 11 -----------
1 file changed, 11 deletions(-)
diff --git a/tests/test_wasyncore.py b/tests/test_wasyncore.py
index 55c4019..a8a480a 100644
--- a/tests/test_wasyncore.py
+++ b/tests/test_wasyncore.py
@@ -1454,17 +1454,6 @@ class Test_dispatcher(unittest.TestCase):
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 = {}
--
Gitee
From 8cba302b1ac08c2874ae179b2af2445e89311bac Mon Sep 17 00:00:00 2001
From: Delta Regeer <bertjw@regeer.org>
Date: Sun, 3 Mar 2024 16:26:22 -0700
Subject: [PATCH 4/6] Don't exit handle_write early -- even if socket is not
connected
Calling handle_close() multiple times does not hurt anything, and is
safe.
---
src/waitress/channel.py | 6 ------
1 file changed, 6 deletions(-)
diff --git a/src/waitress/channel.py b/src/waitress/channel.py
index ea019d3..3860ed5 100644
--- a/src/waitress/channel.py
+++ b/src/waitress/channel.py
@@ -91,13 +91,7 @@ class HTTPChannel(wasyncore.dispatcher):
# 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
--
Gitee
From 63678e652d912e67621580123c603e37c319d8c4 Mon Sep 17 00:00:00 2001
From: Delta Regeer <bertjw@regeer.org>
Date: Sun, 3 Mar 2024 16:35:39 -0700
Subject: [PATCH 5/6] Remove code not used by waitress from vendored asyncore
---
src/waitress/wasyncore.py | 45 ------------------
tests/test_wasyncore.py | 96 ++++++++-------------------------------
2 files changed, 18 insertions(+), 123 deletions(-)
diff --git a/src/waitress/wasyncore.py b/src/waitress/wasyncore.py
index 9794308..49829f9 100644
--- a/src/waitress/wasyncore.py
+++ b/src/waitress/wasyncore.py
@@ -378,23 +378,6 @@ class dispatcher:
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:
@@ -556,34 +539,6 @@ class dispatcher:
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 --git a/tests/test_wasyncore.py b/tests/test_wasyncore.py
index a8a480a..20f68f6 100644
--- a/tests/test_wasyncore.py
+++ b/tests/test_wasyncore.py
@@ -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 @@ class DispatcherTests(unittest.TestCase):
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 @@ class BaseClient(BaseTestHandler):
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
@@ -1489,13 +1451,6 @@ class Test_dispatcher(unittest.TestCase):
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 = {}
@@ -1664,21 +1619,6 @@ class Test_dispatcher(unittest.TestCase):
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
--
Gitee
From 9d99c89ae4aa8449313eea210a5ec9f3994a87b2 Mon Sep 17 00:00:00 2001
From: Delta Regeer <bertjw@regeer.org>
Date: Sun, 3 Mar 2024 16:37:12 -0700
Subject: [PATCH 6/6] When closing the socket, set it to None
This avoids calling close() twice on the same socket if self.close() or
self.handle_close() is called multiple times
---
src/waitress/wasyncore.py | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/src/waitress/wasyncore.py b/src/waitress/wasyncore.py
index 49829f9..f42ee37 100644
--- a/src/waitress/wasyncore.py
+++ b/src/waitress/wasyncore.py
@@ -436,6 +436,8 @@ class dispatcher:
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.
@@ -486,7 +488,11 @@ class dispatcher:
# 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
--
Gitee

392
cve-2022-24761.diff Normal file
View File

@ -0,0 +1,392 @@
diff -Nru waitress-2.0.0/src/waitress/parser.py waitress-2.0.0.fixed/src/waitress/parser.py
--- waitress-2.0.0/src/waitress/parser.py 2021-03-08 07:24:23.000000000 +0000
+++ waitress-2.0.0.fixed/src/waitress/parser.py 2022-04-18 08:04:12.173729937 +0000
@@ -23,6 +23,7 @@
from waitress.buffers import OverflowableBuffer
from waitress.receiver import ChunkedReceiver, FixedStreamReceiver
+from waitress.rfc7230 import HEADER_FIELD_RE, ONLY_DIGIT_RE
from waitress.utilities import (
BadRequest,
RequestEntityTooLarge,
@@ -31,8 +32,6 @@
find_double_newline,
)
-from .rfc7230 import HEADER_FIELD
-
def unquote_bytes_to_wsgi(bytestring):
return unquote_to_bytes(bytestring).decode("latin-1")
@@ -221,7 +220,7 @@
headers = self.headers
for line in lines:
- header = HEADER_FIELD.match(line)
+ header = HEADER_FIELD_RE.match(line)
if not header:
raise ParsingError("Invalid header")
@@ -314,11 +313,12 @@
self.connection_close = True
if not self.chunked:
- try:
- cl = int(headers.get("CONTENT_LENGTH", 0))
- except ValueError:
+ cl = headers.get("CONTENT_LENGTH", "0")
+
+ if not ONLY_DIGIT_RE.match(cl.encode("latin-1")):
raise ParsingError("Content-Length is invalid")
+ cl = int(cl)
self.content_length = cl
if cl > 0:
diff -Nru waitress-2.0.0/src/waitress/receiver.py waitress-2.0.0.fixed/src/waitress/receiver.py
--- waitress-2.0.0/src/waitress/receiver.py 2021-03-08 07:24:23.000000000 +0000
+++ waitress-2.0.0.fixed/src/waitress/receiver.py 2022-04-18 08:04:12.173729937 +0000
@@ -14,6 +14,7 @@
"""Data Chunk Receiver
"""
+from waitress.rfc7230 import CHUNK_EXT_RE, ONLY_HEXDIG_RE
from waitress.utilities import BadRequest, find_double_newline
@@ -110,6 +111,7 @@
s = b""
else:
self.chunk_end = b""
+
if pos == 0:
# Chop off the terminating CR LF from the chunk
s = s[2:]
@@ -133,20 +135,32 @@
line = s[:pos]
s = s[pos + 2 :]
self.control_line = b""
- line = line.strip()
if line:
# Begin a new chunk.
semi = line.find(b";")
if semi >= 0:
- # discard extension info.
+ extinfo = line[semi:]
+ valid_ext_info = CHUNK_EXT_RE.match(extinfo)
+
+ if not valid_ext_info:
+ self.error = BadRequest("Invalid chunk extension")
+ self.all_chunks_received = True
+
+ break
+
line = line[:semi]
- try:
- sz = int(line.strip(), 16) # hexadecimal
- except ValueError: # garbage in input
- self.error = BadRequest("garbage in chunked encoding input")
- sz = 0
+
+ if not ONLY_HEXDIG_RE.match(line):
+ self.error = BadRequest("Invalid chunk size")
+ self.all_chunks_received = True
+
+ break
+
+ # Can not fail due to matching against the regular
+ # expression above
+ sz = int(line, 16) # hexadecimal
if sz > 0:
# Start a new chunk.
diff -Nru waitress-2.0.0/src/waitress/rfc7230.py waitress-2.0.0.fixed/src/waitress/rfc7230.py
--- waitress-2.0.0/src/waitress/rfc7230.py 2021-03-08 07:24:23.000000000 +0000
+++ waitress-2.0.0.fixed/src/waitress/rfc7230.py 2022-04-18 08:04:12.173729937 +0000
@@ -5,6 +5,9 @@
import re
+HEXDIG = "[0-9a-fA-F]"
+DIGIT = "[0-9]"
+
WS = "[ \t]"
OWS = WS + "{0,}?"
RWS = WS + "{1,}?"
@@ -25,6 +28,12 @@
# ; visible (printing) characters
VCHAR = r"\x21-\x7e"
+# The '\\' between \x5b and \x5d is needed to escape \x5d (']')
+QDTEXT = "[\t \x21\x23-\x5b\\\x5d-\x7e" + OBS_TEXT + "]"
+
+QUOTED_PAIR = r"\\" + "([\t " + VCHAR + OBS_TEXT + "])"
+QUOTED_STRING = '"(?:(?:' + QDTEXT + ")|(?:" + QUOTED_PAIR + '))*"'
+
# header-field = field-name ":" OWS field-value OWS
# field-name = token
# field-value = *( field-content / obs-fold )
@@ -43,8 +52,24 @@
# Which allows the field value here to just see if there is even a value in the first place
FIELD_VALUE = "(?:" + FIELD_CONTENT + ")?"
-HEADER_FIELD = re.compile(
+# chunk-ext = *( ";" chunk-ext-name [ "=" chunk-ext-val ] )
+# chunk-ext-name = token
+# chunk-ext-val = token / quoted-string
+
+CHUNK_EXT_NAME = TOKEN
+CHUNK_EXT_VAL = "(?:" + TOKEN + ")|(?:" + QUOTED_STRING + ")"
+CHUNK_EXT = (
+ "(?:;(?P<extension>" + CHUNK_EXT_NAME + ")(?:=(?P<value>" + CHUNK_EXT_VAL + "))?)*"
+)
+
+# Pre-compiled regular expressions for use elsewhere
+ONLY_HEXDIG_RE = re.compile(("^" + HEXDIG + "+$").encode("latin-1"))
+ONLY_DIGIT_RE = re.compile(("^" + DIGIT + "+$").encode("latin-1"))
+HEADER_FIELD_RE = re.compile(
(
"^(?P<name>" + TOKEN + "):" + OWS + "(?P<value>" + FIELD_VALUE + ")" + OWS + "$"
).encode("latin-1")
)
+QUOTED_PAIR_RE = re.compile(QUOTED_PAIR)
+QUOTED_STRING_RE = re.compile(QUOTED_STRING)
+CHUNK_EXT_RE = re.compile(("^" + CHUNK_EXT + "$").encode("latin-1"))
diff -Nru waitress-2.0.0/src/waitress/utilities.py waitress-2.0.0.fixed/src/waitress/utilities.py
--- waitress-2.0.0/src/waitress/utilities.py 2021-03-08 07:24:23.000000000 +0000
+++ waitress-2.0.0.fixed/src/waitress/utilities.py 2022-04-18 08:04:12.173729937 +0000
@@ -22,7 +22,7 @@
import stat
import time
-from .rfc7230 import OBS_TEXT, VCHAR
+from .rfc7230 import QUOTED_PAIR_RE, QUOTED_STRING_RE
logger = logging.getLogger("waitress")
queue_logger = logging.getLogger("waitress.queue")
@@ -216,32 +216,10 @@
return retval
-# RFC 5234 Appendix B.1 "Core Rules":
-# VCHAR = %x21-7E
-# ; visible (printing) characters
-vchar_re = VCHAR
-
-# RFC 7230 Section 3.2.6 "Field Value Components":
-# quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
-# qdtext = HTAB / SP /%x21 / %x23-5B / %x5D-7E / obs-text
-# obs-text = %x80-FF
-# quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
-obs_text_re = OBS_TEXT
-
-# The '\\' between \x5b and \x5d is needed to escape \x5d (']')
-qdtext_re = "[\t \x21\x23-\x5b\\\x5d-\x7e" + obs_text_re + "]"
-
-quoted_pair_re = r"\\" + "([\t " + vchar_re + obs_text_re + "])"
-quoted_string_re = '"(?:(?:' + qdtext_re + ")|(?:" + quoted_pair_re + '))*"'
-
-quoted_string = re.compile(quoted_string_re)
-quoted_pair = re.compile(quoted_pair_re)
-
-
def undquote(value):
if value.startswith('"') and value.endswith('"'):
# So it claims to be DQUOTE'ed, let's validate that
- matches = quoted_string.match(value)
+ matches = QUOTED_STRING_RE.match(value)
if matches and matches.end() == len(value):
# Remove the DQUOTE's from the value
@@ -249,7 +227,7 @@
# Remove all backslashes that are followed by a valid vchar or
# obs-text
- value = quoted_pair.sub(r"\1", value)
+ value = QUOTED_PAIR_RE.sub(r"\1", value)
return value
elif not value.startswith('"') and not value.endswith('"'):
diff -Nru waitress-2.0.0/tests/test_functional.py waitress-2.0.0.fixed/tests/test_functional.py
--- waitress-2.0.0/tests/test_functional.py 2021-03-08 07:24:23.000000000 +0000
+++ waitress-2.0.0.fixed/tests/test_functional.py 2022-04-18 08:04:12.173729937 +0000
@@ -312,7 +312,7 @@
self.assertFalse("transfer-encoding" in headers)
def test_chunking_request_with_content(self):
- control_line = b"20;\r\n" # 20 hex = 32 dec
+ control_line = b"20\r\n" # 20 hex = 32 dec
s = b"This string has 32 characters.\r\n"
expected = s * 12
header = b"GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
@@ -332,7 +332,7 @@
self.assertFalse("transfer-encoding" in headers)
def test_broken_chunked_encoding(self):
- control_line = b"20;\r\n" # 20 hex = 32 dec
+ control_line = b"20\r\n" # 20 hex = 32 dec
s = b"This string has 32 characters.\r\n"
to_send = b"GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
to_send += control_line + s + b"\r\n"
@@ -355,8 +355,52 @@
self.send_check_error(to_send)
self.assertRaises(ConnectionClosed, read_http, fp)
+ def test_broken_chunked_encoding_invalid_hex(self):
+ control_line = b"0x20\r\n" # 20 hex = 32 dec
+ s = b"This string has 32 characters.\r\n"
+ to_send = b"GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
+ to_send += control_line + s + b"\r\n"
+ self.connect()
+ self.sock.send(to_send)
+ with self.sock.makefile("rb", 0) as fp:
+ line, headers, response_body = read_http(fp)
+ self.assertline(line, "400", "Bad Request", "HTTP/1.1")
+ cl = int(headers["content-length"])
+ self.assertEqual(cl, len(response_body))
+ self.assertIn(b"Invalid chunk size", response_body)
+ self.assertEqual(
+ sorted(headers.keys()),
+ ["connection", "content-length", "content-type", "date", "server"],
+ )
+ self.assertEqual(headers["content-type"], "text/plain")
+ # connection has been closed
+ self.send_check_error(to_send)
+ self.assertRaises(ConnectionClosed, read_http, fp)
+
+ def test_broken_chunked_encoding_invalid_extension(self):
+ control_line = b"20;invalid=\r\n" # 20 hex = 32 dec
+ s = b"This string has 32 characters.\r\n"
+ to_send = b"GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
+ to_send += control_line + s + b"\r\n"
+ self.connect()
+ self.sock.send(to_send)
+ with self.sock.makefile("rb", 0) as fp:
+ line, headers, response_body = read_http(fp)
+ self.assertline(line, "400", "Bad Request", "HTTP/1.1")
+ cl = int(headers["content-length"])
+ self.assertEqual(cl, len(response_body))
+ self.assertIn(b"Invalid chunk extension", response_body)
+ self.assertEqual(
+ sorted(headers.keys()),
+ ["connection", "content-length", "content-type", "date", "server"],
+ )
+ self.assertEqual(headers["content-type"], "text/plain")
+ # connection has been closed
+ self.send_check_error(to_send)
+ self.assertRaises(ConnectionClosed, read_http, fp)
+
def test_broken_chunked_encoding_missing_chunk_end(self):
- control_line = b"20;\r\n" # 20 hex = 32 dec
+ control_line = b"20\r\n" # 20 hex = 32 dec
s = b"This string has 32 characters.\r\n"
to_send = b"GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
to_send += control_line + s
diff -Nru waitress-2.0.0/tests/test_parser.py waitress-2.0.0.fixed/tests/test_parser.py
--- waitress-2.0.0/tests/test_parser.py 2021-03-08 07:24:23.000000000 +0000
+++ waitress-2.0.0.fixed/tests/test_parser.py 2022-04-18 08:04:12.173729937 +0000
@@ -155,7 +155,7 @@
b"Transfer-Encoding: chunked\r\n"
b"X-Foo: 1\r\n"
b"\r\n"
- b"1d;\r\n"
+ b"1d\r\n"
b"This string has 29 characters\r\n"
b"0\r\n\r\n"
)
@@ -188,6 +188,26 @@
try:
self.parser.parse_header(data)
+ except ParsingError as e:
+ self.assertIn("Content-Length is invalid", e.args[0])
+ else: # pragma: nocover
+ self.assertTrue(False)
+
+ def test_parse_header_bad_content_length_plus(self):
+ data = b"GET /foobar HTTP/8.4\r\ncontent-length: +10\r\n"
+
+ try:
+ self.parser.parse_header(data)
+ except ParsingError as e:
+ self.assertIn("Content-Length is invalid", e.args[0])
+ else: # pragma: nocover
+ self.assertTrue(False)
+
+ def test_parse_header_bad_content_length_minus(self):
+ data = b"GET /foobar HTTP/8.4\r\ncontent-length: -10\r\n"
+
+ try:
+ self.parser.parse_header(data)
except ParsingError as e:
self.assertIn("Content-Length is invalid", e.args[0])
else: # pragma: nocover
diff -Nru waitress-2.0.0/tests/test_receiver.py waitress-2.0.0.fixed/tests/test_receiver.py
--- waitress-2.0.0/tests/test_receiver.py 2021-03-08 07:24:23.000000000 +0000
+++ waitress-2.0.0.fixed/tests/test_receiver.py 2022-04-18 08:04:12.173729937 +0000
@@ -1,5 +1,7 @@
import unittest
+import pytest
+
class TestFixedStreamReceiver(unittest.TestCase):
def _makeOne(self, cl, buf):
@@ -226,6 +228,55 @@
self.assertEqual(inst.error, None)
+class TestChunkedReceiverParametrized:
+ def _makeOne(self, buf):
+ from waitress.receiver import ChunkedReceiver
+
+ return ChunkedReceiver(buf)
+
+ @pytest.mark.parametrize(
+ "invalid_extension", [b"\n", b"invalid=", b"\r", b"invalid = true"]
+ )
+ def test_received_invalid_extensions(self, invalid_extension):
+ from waitress.utilities import BadRequest
+
+ buf = DummyBuffer()
+ inst = self._makeOne(buf)
+ data = b"4;" + invalid_extension + b"\r\ntest\r\n"
+ result = inst.received(data)
+ assert result == len(data)
+ assert inst.error.__class__ == BadRequest
+ assert inst.error.body == "Invalid chunk extension"
+
+ @pytest.mark.parametrize(
+ "valid_extension", [b"test", b"valid=true", b"valid=true;other=true"]
+ )
+ def test_received_valid_extensions(self, valid_extension):
+ # While waitress may ignore extensions in Chunked Encoding, we do want
+ # to make sure that we don't fail when we do encounter one that is
+ # valid
+ buf = DummyBuffer()
+ inst = self._makeOne(buf)
+ data = b"4;" + valid_extension + b"\r\ntest\r\n"
+ result = inst.received(data)
+ assert result == len(data)
+ assert inst.error == None
+
+ @pytest.mark.parametrize(
+ "invalid_size", [b"0x04", b"+0x04", b"x04", b"+04", b" 04", b" 0x04"]
+ )
+ def test_received_invalid_size(self, invalid_size):
+ from waitress.utilities import BadRequest
+
+ buf = DummyBuffer()
+ inst = self._makeOne(buf)
+ data = invalid_size + b"\r\ntest\r\n"
+ result = inst.received(data)
+ assert result == len(data)
+ assert inst.error.__class__ == BadRequest
+ assert inst.error.body == "Invalid chunk size"
+
+
class DummyBuffer:
def __init__(self, data=None):
if data is None:

View File

@ -1,12 +1,15 @@
%global _docdir_fmt %{name}
Name: python-waitress
Version: 1.4.4
Release: 2
Version: 2.0.0
Release: 5
Summary: A WSGI server for Python 2 and 3
License: ZPLv2.1
URL: https://github.com/Pylons/waitress
Source0: https://github.com/Pylons/waitress/archive/v%{version}/waitress-%{version}.tar.gz
Patch0: cve-2022-24761.diff
Patch1: CVE-2024-49768.patch
Patch2: CVE-2024-49769.patch
BuildArch: noarch
%description
@ -19,7 +22,7 @@ on PyPy 1.6.0+ on UNIX. It supports HTTP/1.0 and HTTP/1.1.
%package -n python3-waitress
%{?python_provide:%python_provide python3-waitress}
Summary: A WSGI server for Python 2 and 3
BuildRequires: python3-devel, python3-setuptools, python3-nose, python3-coverage
BuildRequires: python3-devel, python3-setuptools, python3-coverage
%description -n python3-waitress
Waitress is meant to be a production-quality pure-Python WSGI server
@ -29,7 +32,7 @@ and Windows under Python 2.7+ and Python 3.5+. It is also known to run
on PyPy 1.6.0+ on UNIX. It supports HTTP/1.0 and HTTP/1.1.
%prep
%autosetup -n waitress-%{version}
%autosetup -n waitress-%{version} -p1
%build
%py3_build
@ -44,6 +47,21 @@ on PyPy 1.6.0+ on UNIX. It supports HTTP/1.0 and HTTP/1.1.
%{python3_sitelib}/*
%changelog
* Wed Oct 30 2024 liningjie <liningjie@xfusion.com> - 2.0.0-5
- Fix CVE-2024-49769
* Tue Oct 29 2024 liningjie <liningjie@xfusion.com> - 2.0.0-4
- Fix CVE-2024-49768
* Mon Apr 18 2022 Shinwell_Hu <micromotive@qq.com> - 2.0.0-3
- Backport from 2.1.1 to fix CVE-2022-24761
* Tue Jan 11 2022 wulei <wulei80@huawei.com> - 2.0.0-2
- Remove nose dependency
* Thu Jul 08 2021 suoxiaocong <suoxiaocong@kylinos.com> - 2.0.0-1
- update package to 2.0.0
* Sat Aug 22 2020 tianwei <tianwei12@huawei.com> - 1.4.4-2
- delete python2

Binary file not shown.

BIN
waitress-2.0.0.tar.gz Normal file

Binary file not shown.