You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
113 lines
4.6 KiB
113 lines
4.6 KiB
6 months ago
|
From 4f0c74f6aab47c599d33d36cd783b5fa330384d9 Mon Sep 17 00:00:00 2001
|
||
|
From: Bert JW Regeer <bertjw@regeer.org>
|
||
|
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
|
||
|
|