Skip to content

Commit a6cdc6d

Browse files
committed
fix: Hardened header parsing.
Raise if there is more than one Content-Length header per segment. Improve tests a bit. Minor refactoring.
1 parent 90a7297 commit a6cdc6d

File tree

2 files changed

+28
-13
lines changed

2 files changed

+28
-13
lines changed

multipart.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -671,19 +671,21 @@ def _on_segment_headerline(self, line: Union[bytes, bytearray]):
671671
if len(self._segment_headerlist) >= self.max_header_count:
672672
raise ParserLimitReached("Maximum segment header count exceeded")
673673

674-
# Decode headers into header name and value
674+
# Decode headerline into normalized (name, value) pairs
675675
try:
676676
name, col, value = line.decode(self.header_charset).partition(":")
677-
name = name.strip().title()
678-
if not col or not name:
679-
raise ParserError("Malformed segment header")
680-
if not (name in _KNOWN_HEADERS or _re_hname.fullmatch(name)):
681-
raise ParserError("Invalid segment header name")
682-
value = value.strip()
683677
except UnicodeDecodeError as err:
684678
raise ParserError("Segment header failed to decode", err)
679+
if not col:
680+
raise ParserError("Malformed segment header")
681+
name = name.strip().title()
682+
value = value.strip()
683+
if not (name in _KNOWN_HEADERS or _re_hname.fullmatch(name)):
684+
raise ParserError("Invalid segment header name")
685685

686686
if name == "Content-Length":
687+
if self._segment_limit >= 0:
688+
raise ParserError("Multiple segment Content-Length headers")
687689
try:
688690
content_length = int(value)
689691
if content_length < 0 or str(content_length) != value:

test/test_push_parser.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -195,21 +195,24 @@ def test_header_continuation_long(self):
195195
with self.assertParseError("Maximum segment header length exceeded"):
196196
self.parse(b"\tmoooooooooooooooooooooooooore value\r\n")
197197

198-
def test_header_bad_name(self):
199-
self.reset()
198+
def test_header_no_colon(self):
200199
with self.assertParseError("Malformed segment header"):
201200
self.parse(b"--boundary\r\nno-colon\r\n\r\ndata\r\n--boundary--")
202-
self.reset()
203-
with self.assertParseError("Malformed segment header"):
201+
202+
def test_header_empty_name(self):
203+
with self.assertParseError("Invalid segment header name"):
204204
self.parse(b"--boundary\r\n:empty-name\r\n\r\ndata\r\n--boundary--")
205+
206+
def test_header_bad_name(self):
205207
for badchar in (b" ", b"\0", b"\r", b"\n", "ö".encode("utf8")):
206208
self.reset()
207209
with self.assertParseError("Invalid segment header name"):
208210
self.parse(
209211
b"--boundary\r\ninvalid%sname:value\r\n\r\ndata\r\n--boundary--"
210212
% badchar
211213
)
212-
self.reset()
214+
215+
def test_header_bad_unicode(self):
213216
with self.assertParseError("Segment header failed to decode"):
214217
self.parse(
215218
b"--boundary\r\ninvalid\xc3\x28:value\r\n\r\ndata\r\n--boundary--"
@@ -312,7 +315,17 @@ def test_segment_clen_bad_value(self):
312315
self.parse("x" * 4)
313316
self.parse("\r\n--boundary--")
314317

315-
def test_content_length_limit(self):
318+
def test_segment_clen_repeated(self):
319+
self.parse("--boundary\r\n")
320+
self.parse("Content-Disposition: form-data; name=foo\r\n")
321+
with self.assertParseError("Multiple segment Content-Length headers"):
322+
self.parse(f"Content-Length: 1024\r\n")
323+
self.parse(f"Content-Length: 1024\r\n")
324+
self.parse("\r\n")
325+
self.parse("x" * 1024)
326+
self.parse("\r\n--boundary--")
327+
328+
def test_segment_clen_limit(self):
316329
self.reset(max_segment_size = 1024)
317330
self.parse("--boundary\r\n")
318331
self.parse("Content-Disposition: form-data; name=foo\r\n")

0 commit comments

Comments
 (0)