VYPR
Medium severity5.3NVD Advisory· Published Apr 18, 2026· Updated Apr 24, 2026

CVE-2026-40347

CVE-2026-40347

Description

Python-Multipart before 0.0.26 is vulnerable to denial of service via crafted multipart/form-data requests with large preamble or epilogue sections.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Python-Multipart before 0.0.26 is vulnerable to denial of service via crafted multipart/form-data requests with large preamble or epilogue sections.

Python-Multipart, a streaming multipart parser for Python, contains a denial of service vulnerability in versions prior to 0.0.26. The root cause lies in two inefficient parsing paths: the parser handles leading CR and LF bytes inefficiently while searching for the first multipart boundary, and it continues processing trailing epilogue data after the closing boundary instead of discarding it immediately [1][4].

An attacker can exploit this by sending crafted multipart/form-data requests with oversized preamble or epilogue sections. No special authentication or network position is required beyond the ability to send HTTP requests to an endpoint that parses multipart data [4].

The impact is excessive CPU consumption during request parsing, which reduces the server's request-handling capacity and delays legitimate requests. While this typically does not result in a complete denial of service for the entire application, it degrades availability and can be used to exhaust resources [4].

Mitigation is available by upgrading to version 0.0.26 or later. The fix skips ahead to the next boundary candidate when processing leading CR/LF data and immediately discards epilogue data after the closing boundary [3][4].

AI Insight generated on May 18, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
python-multipartPyPI
< 0.0.260.0.26

Affected products

22

Patches

2
d4452a78bbde

Silently discard epilogue data after the closing boundary (#259)

https://github.com/Kludex/python-multipartMarcelo TrylesinskiApr 10, 2026Fixed in 0.0.26via llm-release-walk
3 files changed · +32 26
  • python_multipart/multipart.py+2 6 modified
    @@ -1417,12 +1417,8 @@ def data_callback(name: CallbackName, end_i: int, remaining: bool = False) -> No
                         state = MultipartState.END
     
                 elif state == MultipartState.END:
    -                # Don't do anything if chunk ends with CRLF.
    -                if c == CR and i + 1 < length and data[i + 1] == LF:
    -                    i += 2
    -                    continue
    -                # Skip data after the last boundary.
    -                self.logger.warning("Skipping data after last boundary")
    +                # Silently discard any epilogue data (RFC 2046 section 5.1.1 allows a CRLF and optional
    +                # epilogue after the closing boundary). Django and Werkzeug do the same.
                     i = length
                     break
     
    
  • tests/test_data/http/single_field_with_trailer.http+2 2 modified
    @@ -3,5 +3,5 @@ Content-Disposition: form-data; name="field"
     
    
     This is a test.
    
     ------WebKitFormBoundaryTkr3kCBQlBe1nrhc--
    
    -this trailer causes a warning
    
    -but should be ignored
    \ No newline at end of file
    +this trailer is epilogue data
    
    +and should be silently ignored
    \ No newline at end of file
    
  • tests/test_multipart.py+28 18 modified
    @@ -1,6 +1,5 @@
     from __future__ import annotations
     
    -import logging
     import os
     import random
     import sys
    @@ -734,6 +733,14 @@ def test_not_aligned(self) -> None:
         "single_field_single_file",
     ]
     
    +EPILOGUE_TEST_HEAD = (
    +    "--boundary\r\n"
    +    'Content-Disposition: form-data; name="file"; filename="filename.txt"\r\n'
    +    "Content-Type: text/plain\r\n\r\n"
    +    "hello\r\n"
    +    "--boundary--"
    +).encode("latin-1")
    +
     
     def split_all(val: bytes) -> Iterator[tuple[bytes, bytes]]:
         """
    @@ -1386,7 +1393,7 @@ def on_file(f: File) -> None:
             self.assert_file_data(files[0], b"hello")
     
         def test_multipart_parser_data_after_last_boundary(self) -> None:
    -        """This test makes sure that the parser does not handle when there is junk data after the last boundary."""
    +        """Parser must short-circuit on arbitrary epilogue data after the closing boundary (no O(N) scan)."""
             num = 50_000_000
             data = (
                 "--boundary\r\n"
    @@ -1404,29 +1411,32 @@ def on_file(f: File) -> None:
             f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary")
             f.write(data.encode("latin-1"))
     
    -    @pytest.fixture(autouse=True)
    -    def inject_fixtures(self, caplog: pytest.LogCaptureFixture) -> None:
    -        self._caplog = caplog
    -
    -    def test_multipart_parser_data_end_with_crlf_without_warnings(self) -> None:
    -        """This test makes sure that the parser does not handle when the data ends with a CRLF."""
    -        data = (
    -            "--boundary\r\n"
    -            'Content-Disposition: form-data; name="file"; filename="filename.txt"\r\n'
    -            "Content-Type: text/plain\r\n\r\n"
    -            "hello\r\n"
    -            "--boundary--\r\n"
    -        )
    +    @parametrize(
    +        "chunks",
    +        [
    +            [EPILOGUE_TEST_HEAD + b"\r\n"],
    +            [EPILOGUE_TEST_HEAD + b"\r", b"\n"],
    +            [EPILOGUE_TEST_HEAD, b"\r\n"],
    +            [EPILOGUE_TEST_HEAD + b"\r\n--boundary\r\nthis is not a valid header\r\n\r\nnot a real part"],
    +        ],
    +    )
    +    def test_multipart_parser_ignores_epilogue(self, chunks: list[bytes]) -> None:
    +        """Epilogue data after the closing boundary must be ignored.
     
    +        Covers both the single-chunk case and the case where trailing CRLF is split across `write()` calls.
    +        The final case asserts that epilogue bytes are not parsed or validated.
    +        """
             files: list[File] = []
     
             def on_file(f: File) -> None:
                 files.append(f)
     
             f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary")
    -        with self._caplog.at_level(logging.WARNING):
    -            f.write(data.encode("latin-1"))
    -            assert len(self._caplog.records) == 0
    +        for chunk in chunks:
    +            f.write(chunk)
    +
    +        assert len(files) == 1
    +        self.assert_file_data(files[0], b"hello")
     
         def test_max_size_multipart(self) -> None:
             # Load test data.
    
6a7b76dd2653

Skip preamble before first multipart boundary (#262)

https://github.com/Kludex/python-multipartMarcelo TrylesinskiApr 10, 2026Fixed in 0.0.26via llm-release-walk
2 files changed · +47 12
  • python_multipart/multipart.py+5 1 modified
    @@ -1105,7 +1105,11 @@ def data_callback(name: CallbackName, end_i: int, remaining: bool = False) -> No
                 if state == MultipartState.START:
                     # Skip leading newlines
                     if c == CR or c == LF:
    -                    i += 1
    +                    i = data.find(b"-", i)
    +                    if i == -1:
    +                        # No boundary candidate in this chunk, so ignore the content after the leading CR/LF.
    +                        i = length
    +                        break
                         continue
     
                     # index is used as in index into our boundary.  Set to 0.
    
  • tests/test_multipart.py+42 11 modified
    @@ -1335,24 +1335,55 @@ def on_field(f: Field) -> None:
             self.assertEqual(fields[2].field_name, b"baz")
             self.assertEqual(fields[2].value, b"asdf")
     
    -    def test_multipart_parser_newlines_before_first_boundary(self) -> None:
    -        """This test makes sure that the parser does not handle when there is junk data after the last boundary."""
    -        num = 5_000_000
    -        data = (
    -            "\r\n" * num + "--boundary\r\n"
    -            'Content-Disposition: form-data; name="file"; filename="filename.txt"\r\n'
    -            "Content-Type: text/plain\r\n\r\n"
    -            "hello\r\n"
    -            "--boundary--"
    -        )
    +    @parametrize(
    +        "chunks",
    +        [
    +            [
    +                b"\r\nignored preamble\r\n"
    +                + (
    +                    b"--boundary\r\n"
    +                    b'Content-Disposition: form-data; name="file"; filename="filename.txt"\r\n'
    +                    b"Content-Type: text/plain\r\n\r\n"
    +                    b"hello\r\n"
    +                    b"--boundary--"
    +                )
    +            ],
    +            [
    +                b"\r\n" * 5_000_000
    +                + (
    +                    b"--boundary\r\n"
    +                    b'Content-Disposition: form-data; name="file"; filename="filename.txt"\r\n'
    +                    b"Content-Type: text/plain\r\n\r\n"
    +                    b"hello\r\n"
    +                    b"--boundary--"
    +                )
    +            ],
    +            [
    +                b"\r\n" * 5_000_000,
    +                (
    +                    b"--boundary\r\n"
    +                    b'Content-Disposition: form-data; name="file"; filename="filename.txt"\r\n'
    +                    b"Content-Type: text/plain\r\n\r\n"
    +                    b"hello\r\n"
    +                    b"--boundary--"
    +                ),
    +            ],
    +        ],
    +    )
    +    def test_multipart_parser_preamble_before_first_boundary(self, chunks: list[bytes]) -> None:
    +        """Parser must not hang or blow up on a preamble before the first boundary."""
     
             files: list[File] = []
     
             def on_file(f: File) -> None:
                 files.append(f)
     
             f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary")
    -        f.write(data.encode("latin-1"))
    +        for chunk in chunks:
    +            f.write(chunk)
    +
    +        assert len(files) == 1
    +        self.assert_file_data(files[0], b"hello")
     
         def test_multipart_parser_data_after_last_boundary(self) -> None:
             """This test makes sure that the parser does not handle when there is junk data after the last boundary."""
    

Vulnerability mechanics

Root cause

"The parser did not properly handle large preamble or epilogue sections in multipart requests, leading to excessive resource consumption."

Attack vector

An attacker can trigger this vulnerability by sending a crafted `multipart/form-data` request. This request would contain a very large preamble section before the first boundary or a large epilogue section after the final boundary. The parser's failure to efficiently skip or discard this data causes excessive CPU and memory usage, leading to a denial of service.

Affected code

The vulnerability exists within the `python_multipart/multipart.py` file. Specifically, the `data_callback` function in the `MultipartState.START` state was modified to efficiently skip preamble data [patch_id=4960293]. Additionally, the `MultipartState.END` state was updated to silently discard epilogue data, preventing the parser from consuming excessive resources [patch_id=4960294].

What the fix does

The patch addresses the vulnerability by modifying how the parser handles preamble and epilogue data. For preamble, it now efficiently skips ahead to the next boundary candidate when processing leading CR/LF data, preventing excessive iteration [patch_id=4960293]. For epilogue data, it is now immediately discarded after the closing boundary, preventing any further processing or resource consumption [patch_id=4960294]. These changes ensure that large preamble and epilogue sections do not lead to denial of service.

Preconditions

  • inputThe request must be of type `multipart/form-data`.

Generated on Jun 5, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.