From 86a7f4d2ea10ab96a3597f64b8662fbd741e2031 Mon Sep 17 00:00:00 2001 From: Renata Ravanelli 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 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 --- 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)