Skip to content

Commit 33bdb41

Browse files
authored
handle chunked input that splits \r\n (#3066)
2 parents 5dceab4 + 77bde33 commit 33bdb41

File tree

4 files changed

+46
-1
lines changed

4 files changed

+46
-1
lines changed

CHANGES.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ Version 3.1.4
66
Unreleased
77

88
- The debugger pin fails after 10 attempts instead of 11. :pr:`3020`
9+
- The multipart form parser handles a ``\r\n`` sequence at a chunk boundary.
10+
:issue:`3065`
911

1012

1113
Version 3.1.3

src/werkzeug/sansio/multipart.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ class MultipartDecoder:
7979
8080
The part data is returned as available to allow the caller to save
8181
the data from memory to disk, if desired.
82+
83+
.. versionchanged:: 3.1.4
84+
Handle chunks that split a``\r\n`` sequence.
8285
"""
8386

8487
def __init__(
@@ -283,6 +286,11 @@ def _parse_data(
283286
data_end = del_index = self.last_newline(data[data_start:]) + data_start
284287
more_data = match is None
285288

289+
# Keep \r\n sequence intact rather than splitting across chunks.
290+
if data_end > data_start and data[data_end - 1] == 0x0D:
291+
data_end -= 1
292+
del_index -= 1
293+
286294
return bytes(data[data_start:data_end]), del_index, more_data
287295

288296

tests/sansio/test_multipart.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,16 @@ def test_decoder_data_start_with_different_newline_positions(
8585
# We want to check up to data start event
8686
while not isinstance(events[-1], Data):
8787
events.append(decoder.next_event())
88-
expected = data_start if data_end == b"" else data_start + b"\r\nBCDE"
88+
89+
expected = data_start
90+
91+
if data_end == b"":
92+
# a split \r\n is deferred to the next event
93+
if expected[-1] == 0x0D:
94+
expected = expected[:-1]
95+
else:
96+
expected += b"\r\nBCDE"
97+
8998
assert events == [
9099
Preamble(data=b""),
91100
File(

tests/test_formparser.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,32 @@ def test_missing_multipart_boundary(self):
152152
)
153153
assert req.form == {}
154154

155+
def test_chunk_split_on_line_break_before_epilogue(self):
156+
data = b"".join(
157+
(
158+
# exactly 64 bytes of header
159+
b"--thirteenbytes\r\n",
160+
b"Content-Disposition: form-data; name=tx3065\r\n\r\n",
161+
# payload that fills 65535 bytes together with the header
162+
b"\n".join([b"\r" * 31] * 2045 + [b"y" * 31]),
163+
# This newline is split by the first chunk
164+
b"\r\n",
165+
# extra payload that also has the final newline split exactly
166+
# at the chunk size.
167+
b"\n".join([b"\r" * 31] * 2047 + [b"x" * 30]),
168+
b"\r\n--thirteenbytes--",
169+
)
170+
)
171+
req = Request.from_values(
172+
input_stream=io.BytesIO(data),
173+
content_length=len(data),
174+
content_type="multipart/form-data; boundary=thirteenbytes",
175+
method="POST",
176+
)
177+
assert len(req.form["tx3065"]) == (131072 - 64 - 1)
178+
assert req.form["tx3065"][-1] == "x"
179+
assert req.form["tx3065"][65470:65473] == "y\r\n"
180+
155181
def test_parse_form_data_put_without_content(self):
156182
# A PUT without a Content-Type header returns empty data
157183

0 commit comments

Comments
 (0)