From 4f0c74f6aab47c599d33d36cd783b5fa330384d9 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sat, 12 Mar 2022 18:48:26 -0700 Subject: [PATCH 5/8] Validate chunk size in Chunked Encoding are HEXDIG RFC7230 states that a chunk-size should be 1*HEXDIG, this is now validated before passing the resulting string to int() which would also parse other formats for hex, such as: `0x01` as `1` and `+0x01` as `1`. This would lead to a potential for a frontend proxy server and waitress to disagree on where a chunk started and ended, thereby potentially leading to request smuggling. With the increased validation if the size is not just hex digits, Waitress now returns a Bad Request and stops processing the request. (cherry picked from commit d9bdfa0cf210f6daf017d7c5a3cc149bdec8a9a7) --- waitress/receiver.py | 19 ++++++++++++++----- waitress/tests/test_functional.py | 22 ++++++++++++++++++++++ waitress/tests/test_receiver.py | 12 ++++++++++++ 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/waitress/receiver.py b/waitress/receiver.py index 106dbc7..9e4bffe 100644 --- a/waitress/receiver.py +++ b/waitress/receiver.py @@ -150,12 +150,21 @@ class ChunkedReceiver(object): 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 + + # Remove any whitespace + line = line.strip() + + 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.strip(), 16) # hexadecimal if sz > 0: # Start a new chunk. diff --git a/waitress/tests/test_functional.py b/waitress/tests/test_functional.py index b1aac96..a7421c6 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_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) + 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 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" diff --git a/waitress/tests/test_receiver.py b/waitress/tests/test_receiver.py index e5d31a3..b539264 100644 --- a/waitress/tests/test_receiver.py +++ b/waitress/tests/test_receiver.py @@ -256,6 +256,18 @@ class TestChunkedReceiverParametrized: assert result == len(data) assert inst.error == None + def test_received_invalid_size(self, invalid_size): + from waitress.utilities import BadRequest + + for invalid_size in [b"0x04", b"+0x04", b"x04", b"+04"]: + 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(object): def __init__(self, data=None): -- 2.45.2