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.
153 lines
5.6 KiB
153 lines
5.6 KiB
1 year ago
|
From 86a7f4d2ea10ab96a3597f64b8662fbd741e2031 Mon Sep 17 00:00:00 2001
|
||
|
From: Renata Ravanelli <renata.ravanelli@gmail.com>
|
||
|
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 <bertjw@regeer.org>
|
||
|
|
||
|
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 <renata.ravanelli@gmail.com>
|
||
|
---
|
||
|
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)
|
||
|
|