Skip to content

Commit d4452a7

Browse files
authored
Silently discard epilogue data after the closing boundary (#259)
1 parent 6a7b76d commit d4452a7

File tree

3 files changed

+32
-26
lines changed

3 files changed

+32
-26
lines changed

python_multipart/multipart.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1417,12 +1417,8 @@ def data_callback(name: CallbackName, end_i: int, remaining: bool = False) -> No
14171417
state = MultipartState.END
14181418

14191419
elif state == MultipartState.END:
1420-
# Don't do anything if chunk ends with CRLF.
1421-
if c == CR and i + 1 < length and data[i + 1] == LF:
1422-
i += 2
1423-
continue
1424-
# Skip data after the last boundary.
1425-
self.logger.warning("Skipping data after last boundary")
1420+
# Silently discard any epilogue data (RFC 2046 section 5.1.1 allows a CRLF and optional
1421+
# epilogue after the closing boundary). Django and Werkzeug do the same.
14261422
i = length
14271423
break
14281424

tests/test_data/http/single_field_with_trailer.http

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ Content-Disposition: form-data; name="field"
33

44
This is a test.
55
------WebKitFormBoundaryTkr3kCBQlBe1nrhc--
6-
this trailer causes a warning
7-
but should be ignored
6+
this trailer is epilogue data
7+
and should be silently ignored

tests/test_multipart.py

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import annotations
22

3-
import logging
43
import os
54
import random
65
import sys
@@ -734,6 +733,14 @@ def test_not_aligned(self) -> None:
734733
"single_field_single_file",
735734
]
736735

736+
EPILOGUE_TEST_HEAD = (
737+
"--boundary\r\n"
738+
'Content-Disposition: form-data; name="file"; filename="filename.txt"\r\n'
739+
"Content-Type: text/plain\r\n\r\n"
740+
"hello\r\n"
741+
"--boundary--"
742+
).encode("latin-1")
743+
737744

738745
def split_all(val: bytes) -> Iterator[tuple[bytes, bytes]]:
739746
"""
@@ -1386,7 +1393,7 @@ def on_file(f: File) -> None:
13861393
self.assert_file_data(files[0], b"hello")
13871394

13881395
def test_multipart_parser_data_after_last_boundary(self) -> None:
1389-
"""This test makes sure that the parser does not handle when there is junk data after the last boundary."""
1396+
"""Parser must short-circuit on arbitrary epilogue data after the closing boundary (no O(N) scan)."""
13901397
num = 50_000_000
13911398
data = (
13921399
"--boundary\r\n"
@@ -1404,29 +1411,32 @@ def on_file(f: File) -> None:
14041411
f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary")
14051412
f.write(data.encode("latin-1"))
14061413

1407-
@pytest.fixture(autouse=True)
1408-
def inject_fixtures(self, caplog: pytest.LogCaptureFixture) -> None:
1409-
self._caplog = caplog
1410-
1411-
def test_multipart_parser_data_end_with_crlf_without_warnings(self) -> None:
1412-
"""This test makes sure that the parser does not handle when the data ends with a CRLF."""
1413-
data = (
1414-
"--boundary\r\n"
1415-
'Content-Disposition: form-data; name="file"; filename="filename.txt"\r\n'
1416-
"Content-Type: text/plain\r\n\r\n"
1417-
"hello\r\n"
1418-
"--boundary--\r\n"
1419-
)
1414+
@parametrize(
1415+
"chunks",
1416+
[
1417+
[EPILOGUE_TEST_HEAD + b"\r\n"],
1418+
[EPILOGUE_TEST_HEAD + b"\r", b"\n"],
1419+
[EPILOGUE_TEST_HEAD, b"\r\n"],
1420+
[EPILOGUE_TEST_HEAD + b"\r\n--boundary\r\nthis is not a valid header\r\n\r\nnot a real part"],
1421+
],
1422+
)
1423+
def test_multipart_parser_ignores_epilogue(self, chunks: list[bytes]) -> None:
1424+
"""Epilogue data after the closing boundary must be ignored.
14201425
1426+
Covers both the single-chunk case and the case where trailing CRLF is split across `write()` calls.
1427+
The final case asserts that epilogue bytes are not parsed or validated.
1428+
"""
14211429
files: list[File] = []
14221430

14231431
def on_file(f: File) -> None:
14241432
files.append(f)
14251433

14261434
f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary")
1427-
with self._caplog.at_level(logging.WARNING):
1428-
f.write(data.encode("latin-1"))
1429-
assert len(self._caplog.records) == 0
1435+
for chunk in chunks:
1436+
f.write(chunk)
1437+
1438+
assert len(files) == 1
1439+
self.assert_file_data(files[0], b"hello")
14301440

14311441
def test_max_size_multipart(self) -> None:
14321442
# Load test data.

0 commit comments

Comments
 (0)