Compare commits
21 Commits
Author | SHA1 | Date |
---|---|---|
Renata Andrade Matos Ravanelli | 74f0475310 | 1 year ago |
Carl George | f30ce063b9 | 2 years ago |
Carl George | c68e6b9057 | 2 years ago |
Carl George | 97a59c22b7 | 2 years ago |
Fedora Release Engineering | 333344a045 | 3 years ago |
Fedora Release Engineering | a94b12020d | 3 years ago |
Python Maint | 4893737370 | 4 years ago |
Fedora Release Engineering | 4e7c932d35 | 4 years ago |
Troy Dawson | e5ce6bf678 | 4 years ago |
Joel Capitao | 3b41398289 | 4 years ago |
Fedora Release Engineering | a1b1668fc8 | 4 years ago |
Miro Hrončok | 20cd130b10 | 5 years ago |
Lorenzo Gil Sanchez | 5d391fb590 | 5 years ago |
Fedora Release Engineering | 6eee6c0d5e | 5 years ago |
Lorenzo Gil Sanchez | b054c6c025 | 5 years ago |
Lorenzo Gil Sanchez | e35378fb8a | 5 years ago |
Lorenzo Gil Sanchez | fd1028f2e2 | 5 years ago |
Kevin Fenzi | 0c9b05ec3b | 5 years ago |
Miro Hrončok | a98bc75492 | 5 years ago |
Miro Hrončok | 74ca1f59f0 | 5 years ago |
Fedora Release Engineering | c348fb5f45 | 5 years ago |
@ -1,135 +0,0 @@
|
|||||||
From 7661d0826c9d0f197e66feed5b306b56c90255c4 Mon Sep 17 00:00:00 2001
|
|
||||||
From: Bert JW Regeer <bertjw@regeer.org>
|
|
||||||
Date: Sat, 12 Mar 2022 18:42:51 -0700
|
|
||||||
Subject: [PATCH 4/8] Error when receiving back Chunk Extension
|
|
||||||
|
|
||||||
Waitress discards chunked extensions and does no further processing on
|
|
||||||
them, however it failed to validate that the chunked encoding extension
|
|
||||||
did not contain invalid data.
|
|
||||||
|
|
||||||
We now validate that if there are any chunked extensions that they are
|
|
||||||
well-formed, if they are not and contain invalid characters, then
|
|
||||||
Waitress will now correctly return a Bad Request and stop any further
|
|
||||||
processing of the request.
|
|
||||||
|
|
||||||
(cherry picked from commit d032a669682838b26d6a1a1b513b9da83b0e0f90)
|
|
||||||
---
|
|
||||||
waitress/receiver.py | 11 ++++++++++-
|
|
||||||
waitress/tests/test_functional.py | 22 ++++++++++++++++++++++
|
|
||||||
waitress/tests/test_receiver.py | 31 +++++++++++++++++++++++++++++++
|
|
||||||
3 files changed, 63 insertions(+), 1 deletion(-)
|
|
||||||
|
|
||||||
diff --git a/waitress/receiver.py b/waitress/receiver.py
|
|
||||||
index 5d1568d..106dbc7 100644
|
|
||||||
--- a/waitress/receiver.py
|
|
||||||
+++ b/waitress/receiver.py
|
|
||||||
@@ -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 @@ class ChunkedReceiver(object):
|
|
||||||
s = b""
|
|
||||||
else:
|
|
||||||
self.chunk_end = b""
|
|
||||||
+
|
|
||||||
if pos == 0:
|
|
||||||
# Chop off the terminating CR LF from the chunk
|
|
||||||
s = s[2:]
|
|
||||||
@@ -140,7 +142,14 @@ class ChunkedReceiver(object):
|
|
||||||
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
|
|
||||||
diff --git a/waitress/tests/test_functional.py b/waitress/tests/test_functional.py
|
|
||||||
index 33f1317..b1aac96 100644
|
|
||||||
--- a/waitress/tests/test_functional.py
|
|
||||||
+++ b/waitress/tests/test_functional.py
|
|
||||||
@@ -343,6 +343,28 @@ class EchoTests(object):
|
|
||||||
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)
|
|
||||||
+ fp = self.sock.makefile("rb", 0)
|
|
||||||
+ 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 = "20\r\n" # 20 hex = 32 dec
|
|
||||||
s = "This string has 32 characters.\r\n"
|
|
||||||
diff --git a/waitress/tests/test_receiver.py b/waitress/tests/test_receiver.py
|
|
||||||
index b4910bb..e5d31a3 100644
|
|
||||||
--- a/waitress/tests/test_receiver.py
|
|
||||||
+++ b/waitress/tests/test_receiver.py
|
|
||||||
@@ -226,6 +226,37 @@ class TestChunkedReceiver(unittest.TestCase):
|
|
||||||
self.assertEqual(inst.error, None)
|
|
||||||
|
|
||||||
|
|
||||||
+class TestChunkedReceiverParametrized:
|
|
||||||
+ def _makeOne(self, buf):
|
|
||||||
+ from waitress.receiver import ChunkedReceiver
|
|
||||||
+
|
|
||||||
+ return ChunkedReceiver(buf)
|
|
||||||
+
|
|
||||||
+ def test_received_invalid_extensions(self):
|
|
||||||
+ from waitress.utilities import BadRequest
|
|
||||||
+
|
|
||||||
+ for invalid_extension in [b"\n", b"invalid=", b"\r", b"invalid = true"]:
|
|
||||||
+ 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"
|
|
||||||
+
|
|
||||||
+ def test_received_valid_extensions(self):
|
|
||||||
+ # 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
|
|
||||||
+ for valid_extension in [b"test", b"valid=true", b"valid=true;other=true"]:
|
|
||||||
+ 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
|
|
||||||
+
|
|
||||||
+
|
|
||||||
class DummyBuffer(object):
|
|
||||||
def __init__(self, data=None):
|
|
||||||
if data is None:
|
|
||||||
--
|
|
||||||
2.45.2
|
|
||||||
|
|
@ -0,0 +1,152 @@
|
|||||||
|
From 86a7f4d2ea10ab96a3597f64b8662fbd741e2031 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Renata Ravanelli <renata.ravanelli@gmail.com>
|
||||||
|
Date: Fri, 15 Sep 2023 12:40:31 -0300
|
||||||
|
Subject: [PATCH 4/6] This patch is a backport of commit: d032a66
|
||||||
|
|
||||||
|
From: Bert JW Regeer <bertjw@regeer.org>
|
||||||
|
|
||||||
|
Date: Sat, 12 Mar 2022 18:42:51 -0700
|
||||||
|
Subject: [PATCH] Error when receiving back Chunk Extension
|
||||||
|
|
||||||
|
Waitress discards chunked extensions and does no further processing on
|
||||||
|
them, however it failed to validate that the chunked encoding extension
|
||||||
|
did not contain invalid data.
|
||||||
|
|
||||||
|
We now validate that if there are any chunked extensions that they are
|
||||||
|
well-formed, if they are not and contain invalid characters, then
|
||||||
|
Waitress will now correctly return a Bad Request and stop any further
|
||||||
|
processing of the request
|
||||||
|
|
||||||
|
Signed-off-by: Renata Ravanelli <renata.ravanelli@gmail.com>
|
||||||
|
---
|
||||||
|
src/waitress/receiver.py | 11 ++++++++++-
|
||||||
|
tests/test_functional.py | 22 ++++++++++++++++++++++
|
||||||
|
tests/test_receiver.py | 37 +++++++++++++++++++++++++++++++++++++
|
||||||
|
3 files changed, 69 insertions(+), 1 deletion(-)
|
||||||
|
|
||||||
|
diff --git a/src/waitress/receiver.py b/src/waitress/receiver.py
|
||||||
|
index 5d1568d..106dbc7 100644
|
||||||
|
--- a/src/waitress/receiver.py
|
||||||
|
+++ b/src/waitress/receiver.py
|
||||||
|
@@ -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 @@ class ChunkedReceiver(object):
|
||||||
|
s = b""
|
||||||
|
else:
|
||||||
|
self.chunk_end = b""
|
||||||
|
+
|
||||||
|
if pos == 0:
|
||||||
|
# Chop off the terminating CR LF from the chunk
|
||||||
|
s = s[2:]
|
||||||
|
@@ -140,7 +142,14 @@ class ChunkedReceiver(object):
|
||||||
|
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
|
||||||
|
diff --git a/tests/test_functional.py b/tests/test_functional.py
|
||||||
|
index 7a54b22..853942c 100644
|
||||||
|
--- a/tests/test_functional.py
|
||||||
|
+++ b/tests/test_functional.py
|
||||||
|
@@ -345,6 +345,28 @@ class EchoTests(object):
|
||||||
|
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 = "20\r\n" # 20 hex = 32 dec
|
||||||
|
s = "This string has 32 characters.\r\n"
|
||||||
|
diff --git a/tests/test_receiver.py b/tests/test_receiver.py
|
||||||
|
index b4910bb..a6261ea 100644
|
||||||
|
--- a/tests/test_receiver.py
|
||||||
|
+++ b/tests/test_receiver.py
|
||||||
|
@@ -1,5 +1,7 @@
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
+import pytest
|
||||||
|
+
|
||||||
|
|
||||||
|
class TestFixedStreamReceiver(unittest.TestCase):
|
||||||
|
def _makeOne(self, cl, buf):
|
||||||
|
@@ -226,6 +228,41 @@ class TestChunkedReceiver(unittest.TestCase):
|
||||||
|
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
|
||||||
|
+
|
||||||
|
+
|
||||||
|
class DummyBuffer(object):
|
||||||
|
def __init__(self, data=None):
|
||||||
|
if data is None:
|
||||||
|
--
|
||||||
|
2.39.2 (Apple Git-143)
|
||||||
|
|
@ -1,43 +0,0 @@
|
|||||||
From 6e0af1e0e01f7c9a9a83431b99a82b0de5c6a5da Mon Sep 17 00:00:00 2001
|
|
||||||
From: Carl George <carlwgeorge@gmail.com>
|
|
||||||
Date: Tue, 25 Jun 2024 22:40:57 -0500
|
|
||||||
Subject: [PATCH 7/8] Backport security fix note
|
|
||||||
|
|
||||||
---
|
|
||||||
CHANGES.txt | 23 +++++++++++++++++++++++
|
|
||||||
1 file changed, 23 insertions(+)
|
|
||||||
|
|
||||||
diff --git a/CHANGES.txt b/CHANGES.txt
|
|
||||||
index 701c2b0..f9d4c42 100644
|
|
||||||
--- a/CHANGES.txt
|
|
||||||
+++ b/CHANGES.txt
|
|
||||||
@@ -1,3 +1,26 @@
|
|
||||||
+Security Bugfix
|
|
||||||
+~~~~~~~~~~~~~~~
|
|
||||||
+
|
|
||||||
+- Waitress now validates that chunked encoding extensions are valid, and don't
|
|
||||||
+ contain invalid characters that are not allowed. They are still skipped/not
|
|
||||||
+ processed, but if they contain invalid data we no longer continue in and
|
|
||||||
+ return a 400 Bad Request. This stops potential HTTP desync/HTTP request
|
|
||||||
+ smuggling. Thanks to Zhang Zeyu for reporting this issue. See
|
|
||||||
+ https://github.com/Pylons/waitress/security/advisories/GHSA-4f7p-27jc-3c36
|
|
||||||
+
|
|
||||||
+- Waitress now validates that the chunk length is only valid hex digits when
|
|
||||||
+ parsing chunked encoding, and values such as ``0x01`` and ``+01`` are no
|
|
||||||
+ longer supported. This stops potential HTTP desync/HTTP request smuggling.
|
|
||||||
+ Thanks to Zhang Zeyu for reporting this issue. See
|
|
||||||
+ https://github.com/Pylons/waitress/security/advisories/GHSA-4f7p-27jc-3c36
|
|
||||||
+
|
|
||||||
+- Waitress now validates that the Content-Length sent by a remote contains only
|
|
||||||
+ digits in accordance with RFC7230 and will return a 400 Bad Request when the
|
|
||||||
+ Content-Length header contains invalid data, such as ``+10`` which would
|
|
||||||
+ previously get parsed as ``10`` and accepted. This stops potential HTTP
|
|
||||||
+ desync/HTTP request smuggling Thanks to Zhang Zeyu for reporting this issue. See
|
|
||||||
+ https://github.com/Pylons/waitress/security/advisories/GHSA-4f7p-27jc-3c36
|
|
||||||
+
|
|
||||||
1.4.3 (2020-02-02)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
--
|
|
||||||
2.45.2
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
|||||||
From 4f0407051486b5e01a148ca53f361dd802d88c59 Mon Sep 17 00:00:00 2001
|
|
||||||
From: Carl George <carlwgeorge@gmail.com>
|
|
||||||
Date: Tue, 25 Jun 2024 22:55:20 -0500
|
|
||||||
Subject: [PATCH 8/8] Skip tests that fail inconsistently during mock build
|
|
||||||
|
|
||||||
---
|
|
||||||
waitress/tests/test_functional.py | 2 ++
|
|
||||||
1 file changed, 2 insertions(+)
|
|
||||||
|
|
||||||
diff --git a/waitress/tests/test_functional.py b/waitress/tests/test_functional.py
|
|
||||||
index a7421c6..d846d06 100644
|
|
||||||
--- a/waitress/tests/test_functional.py
|
|
||||||
+++ b/waitress/tests/test_functional.py
|
|
||||||
@@ -1224,6 +1224,7 @@ class InternalServerErrorTests(object):
|
|
||||||
self.send_check_error(to_send)
|
|
||||||
self.assertRaises(ConnectionClosed, read_http, fp)
|
|
||||||
|
|
||||||
+ @unittest.skip('fails inconsistently during mock build')
|
|
||||||
def test_after_write_cb(self):
|
|
||||||
to_send = "GET /after_write_cb HTTP/1.1\r\n\r\n"
|
|
||||||
to_send = tobytes(to_send)
|
|
||||||
@@ -1237,6 +1238,7 @@ class InternalServerErrorTests(object):
|
|
||||||
self.send_check_error(to_send)
|
|
||||||
self.assertRaises(ConnectionClosed, read_http, fp)
|
|
||||||
|
|
||||||
+ @unittest.skip('fails inconsistently during mock build')
|
|
||||||
def test_in_generator(self):
|
|
||||||
to_send = "GET /in_generator HTTP/1.1\r\n\r\n"
|
|
||||||
to_send = tobytes(to_send)
|
|
||||||
--
|
|
||||||
2.45.2
|
|
||||||
|
|
@ -1 +1 @@
|
|||||||
SHA512 (v1.4.3-nodocs.tar.gz) = c3749376e97d864874b1976b7f9f2688d3b55c56e33a01d968fc59a068a27ea14dd389d8ca4feb211afbfd0bb6848f6b8d483142e0b7a1b403f924fb7cb87f3c
|
SHA512 (v1.4.4-nodocs.tar.gz) = 246e066774f093caf174c2e7a054fedf9d09ce871524f6fdfd86bade89b858ff28ea0fd7347874303e473bf2527919beecc174264d5d8283030ab13c5942ef2d
|
||||||
|
Loading…
Reference in new issue