File tree Expand file tree Collapse file tree 4 files changed +46
-1
lines changed Expand file tree Collapse file tree 4 files changed +46
-1
lines changed Original file line number Diff line number Diff line change @@ -6,6 +6,8 @@ Version 3.1.4
66Unreleased
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
1113Version 3.1.3
Original file line number Diff line number Diff 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
Original file line number Diff line number Diff 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 \n BCDE"
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 \n BCDE"
97+
8998 assert events == [
9099 Preamble (data = b"" ),
91100 File (
Original file line number Diff line number Diff 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
You can’t perform that action at this time.
0 commit comments