Skip to content

Commit d032a66

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

File tree

3 files changed

+69
-1
lines changed

3 files changed

+69
-1
lines changed

src/waitress/receiver.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"""Data Chunk Receiver
1515
"""
1616

17+
from waitress.rfc7230 import CHUNK_EXT_RE, ONLY_HEXDIG_RE
1718
from waitress.utilities import BadRequest, find_double_newline
1819

1920

@@ -110,6 +111,7 @@ def received(self, s):
110111
s = b""
111112
else:
112113
self.chunk_end = b""
114+
113115
if pos == 0:
114116
# Chop off the terminating CR LF from the chunk
115117
s = s[2:]
@@ -140,7 +142,14 @@ def received(self, s):
140142
semi = line.find(b";")
141143

142144
if semi >= 0:
143-
# discard extension info.
145+
extinfo = line[semi:]
146+
valid_ext_info = CHUNK_EXT_RE.match(extinfo)
147+
148+
if not valid_ext_info:
149+
self.error = BadRequest("Invalid chunk extension")
150+
self.all_chunks_received = True
151+
152+
break
144153
line = line[:semi]
145154
try:
146155
sz = int(line.strip(), 16) # hexadecimal

tests/test_functional.py

Lines changed: 22 additions & 0 deletions
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_extension(self):
368+
control_line = b"20;invalid=\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 extension", 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_missing_chunk_end(self):
368390
control_line = b"20\r\n" # 20 hex = 32 dec
369391
s = b"This string has 32 characters.\r\n"

tests/test_receiver.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import unittest
22

3+
import pytest
4+
35

46
class TestFixedStreamReceiver(unittest.TestCase):
57
def _makeOne(self, cl, buf):
@@ -226,6 +228,41 @@ def test_received_multiple_chunks_split(self):
226228
self.assertEqual(inst.error, None)
227229

228230

231+
class TestChunkedReceiverParametrized:
232+
def _makeOne(self, buf):
233+
from waitress.receiver import ChunkedReceiver
234+
235+
return ChunkedReceiver(buf)
236+
237+
@pytest.mark.parametrize(
238+
"invalid_extension", [b"\n", b"invalid=", b"\r", b"invalid = true"]
239+
)
240+
def test_received_invalid_extensions(self, invalid_extension):
241+
from waitress.utilities import BadRequest
242+
243+
buf = DummyBuffer()
244+
inst = self._makeOne(buf)
245+
data = b"4;" + invalid_extension + b"\r\ntest\r\n"
246+
result = inst.received(data)
247+
assert result == len(data)
248+
assert inst.error.__class__ == BadRequest
249+
assert inst.error.body == "Invalid chunk extension"
250+
251+
@pytest.mark.parametrize(
252+
"valid_extension", [b"test", b"valid=true", b"valid=true;other=true"]
253+
)
254+
def test_received_valid_extensions(self, valid_extension):
255+
# While waitress may ignore extensions in Chunked Encoding, we do want
256+
# to make sure that we don't fail when we do encounter one that is
257+
# valid
258+
buf = DummyBuffer()
259+
inst = self._makeOne(buf)
260+
data = b"4;" + valid_extension + b"\r\ntest\r\n"
261+
result = inst.received(data)
262+
assert result == len(data)
263+
assert inst.error == None
264+
265+
229266
class DummyBuffer:
230267
def __init__(self, data=None):
231268
if data is None:

0 commit comments

Comments
 (0)