Skip to content

Commit d9bdfa0

Browse files
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.
1 parent d032a66 commit d9bdfa0

File tree

3 files changed

+48
-5
lines changed

3 files changed

+48
-5
lines changed

src/waitress/receiver.py

+14-5
Original file line numberDiff line numberDiff line change
@@ -150,12 +150,21 @@ def received(self, s):
150150
self.all_chunks_received = True
151151

152152
break
153+
153154
line = line[:semi]
154-
try:
155-
sz = int(line.strip(), 16) # hexadecimal
156-
except ValueError: # garbage in input
157-
self.error = BadRequest("garbage in chunked encoding input")
158-
sz = 0
155+
156+
# Remove any whitespace
157+
line = line.strip()
158+
159+
if not ONLY_HEXDIG_RE.match(line):
160+
self.error = BadRequest("Invalid chunk size")
161+
self.all_chunks_received = True
162+
163+
break
164+
165+
# Can not fail due to matching against the regular
166+
# expression above
167+
sz = int(line.strip(), 16) # hexadecimal
159168

160169
if sz > 0:
161170
# Start a new chunk.

tests/test_functional.py

+22
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,28 @@ def test_broken_chunked_encoding(self):
364364
self.send_check_error(to_send)
365365
self.assertRaises(ConnectionClosed, read_http, fp)
366366

367+
def test_broken_chunked_encoding_invalid_hex(self):
368+
control_line = b"0x20\r\n" # 20 hex = 32 dec
369+
s = b"This string has 32 characters.\r\n"
370+
to_send = b"GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
371+
to_send += control_line + s + b"\r\n"
372+
self.connect()
373+
self.sock.send(to_send)
374+
with self.sock.makefile("rb", 0) as fp:
375+
line, headers, response_body = read_http(fp)
376+
self.assertline(line, "400", "Bad Request", "HTTP/1.1")
377+
cl = int(headers["content-length"])
378+
self.assertEqual(cl, len(response_body))
379+
self.assertIn(b"Invalid chunk size", response_body)
380+
self.assertEqual(
381+
sorted(headers.keys()),
382+
["connection", "content-length", "content-type", "date", "server"],
383+
)
384+
self.assertEqual(headers["content-type"], "text/plain")
385+
# connection has been closed
386+
self.send_check_error(to_send)
387+
self.assertRaises(ConnectionClosed, read_http, fp)
388+
367389
def test_broken_chunked_encoding_invalid_extension(self):
368390
control_line = b"20;invalid=\r\n" # 20 hex = 32 dec
369391
s = b"This string has 32 characters.\r\n"

tests/test_receiver.py

+12
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,18 @@ def test_received_valid_extensions(self, valid_extension):
262262
assert result == len(data)
263263
assert inst.error == None
264264

265+
@pytest.mark.parametrize("invalid_size", [b"0x04", b"+0x04", b"x04", b"+04"])
266+
def test_received_invalid_size(self, invalid_size):
267+
from waitress.utilities import BadRequest
268+
269+
buf = DummyBuffer()
270+
inst = self._makeOne(buf)
271+
data = invalid_size + b"\r\ntest\r\n"
272+
result = inst.received(data)
273+
assert result == len(data)
274+
assert inst.error.__class__ == BadRequest
275+
assert inst.error.body == "Invalid chunk size"
276+
265277

266278
class DummyBuffer:
267279
def __init__(self, data=None):

0 commit comments

Comments
 (0)