VYPR
High severity7.5GHSA Advisory· Published Jun 15, 2026· Updated Jun 15, 2026

python-multipart: Quadratic-time querystring parsing with semicolon separators causes CPU denial of service

CVE-2026-53539

Description

Quadratic-time parsing in python-multipart's QuerystringParser when using semicolon separators allows CPU exhaustion via crafted form bodies.

AI Insight

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

Quadratic-time parsing in python-multipart's QuerystringParser when using semicolon separators allows CPU exhaustion via crafted form bodies.

Vulnerability

In python_multipart/multipart.py, the QuerystringParser locates field separators by first scanning the entire remaining buffer for & and only falling back to ; when no & is found. For a body that uses ; as the separator and contains no &, every field iteration performs a full failed & scan over the remaining buffer before finding the nearby ;. With N semicolon-separated fields in a chunk of size B, this yields O(B^2) byte comparisons per chunk [1][2]. All versions of python-multipart are affected. The parser is reachable through the public QuerystringParser class and the high-level FormParser, create_form_parser, and parse_form APIs for URL-encoded bodies. It is also the parser used by Starlette and FastAPI via request.form() [1][2].

Exploitation

An attacker can submit a small crafted body of the form a;a;a;... (e.g., 1 MiB of repeated a;) with Content-Type: application/x-www-form-urlencoded. No authentication or special network position is required. The parser performs on the order of 10^11 byte comparisons for a 1 MiB body, consuming several seconds of CPU per request [1][2]. A handful of concurrent requests can exhaust worker processes.

Impact

Uncontrolled CPU consumption leading to denial of service. Parsing is synchronous, so a single crafted form body occupies the handling worker for seconds, blocking any other work on that worker until parsing finishes. Sustained concurrent requests can keep workers busy indefinitely [1][2].

Mitigation

No fix has been published in the available references [1][2]. Users should monitor the python-multipart repository for a patched release. As a workaround, consider limiting the size of incoming request bodies or using an alternative parser for application/x-www-form-urlencoded data.

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

Affected products

2

Patches

1
d69df35cd2ca

Treat only `&` as the urlencoded field separator (#290)

https://github.com/Kludex/python-multipartMarcelo TrylesinskiMay 31, 2026Fixed in 0.0.30via llm-release-walk
3 files changed · +14 16
  • CHANGELOG.md+4 0 modified
    @@ -1,5 +1,9 @@
     # Changelog
     
    +## Unreleased
    +
    +* Parse `application/x-www-form-urlencoded` bodies per the WHATWG URL standard, treating only `&` as a field separator.
    +
     ## 0.0.29 (2026-05-17)
     
     * Handle malformed RFC 2231 continuations in `parse_options_header` [#270](https://github.com/Kludex/python-multipart/pull/270).
    
  • python_multipart/multipart.py+7 13 modified
    @@ -126,7 +126,6 @@ class MultipartState(IntEnum):
     SPACE = b" "[0]
     HYPHEN = b"-"[0]
     AMPERSAND = b"&"[0]
    -SEMICOLON = b";"[0]
     LOWER_A = b"a"[0]
     LOWER_Z = b"z"[0]
     NULL = b"\x00"[0]
    @@ -840,13 +839,13 @@ def _internal_write(self, data: bytes, length: int) -> int:
                     # yet reached a separator, and thus, if we do, we need to skip
                     # it as it will be the boundary between fields that's supposed
                     # to be there.
    -                if ch == AMPERSAND or ch == SEMICOLON:
    +                if ch == AMPERSAND:
                         if found_sep:
                             # If we're parsing strictly, we disallow blank chunks.
                             if strict_parsing:
    -                            raise QuerystringParseError("Skipping duplicate ampersand/semicolon at %d" % i, offset=i)
    +                            raise QuerystringParseError("Skipping duplicate ampersand at %d" % i, offset=i)
                             else:
    -                            self.logger.debug("Skipping duplicate ampersand/semicolon at %d", i)
    +                            self.logger.debug("Skipping duplicate ampersand at %d", i)
                         else:
                             # This case is when we're skipping the (first)
                             # separator between fields, so we just set our flag
    @@ -864,17 +863,15 @@ def _internal_write(self, data: bytes, length: int) -> int:
                 elif state == QuerystringState.FIELD_NAME:
                     # Try and find a separator - we ensure that, if we do, we only
                     # look for the equal sign before it.
    -                sep_pos = data.find(b"&", i)
    -                if sep_pos == -1:
    -                    sep_pos = data.find(b";", i)
    +                sep_pos = data.find(b"&", i, length)
     
                     # See if we can find an equals sign in the remaining data.  If
                     # so, we can immediately emit the field name and jump to the
                     # data state.
                     if sep_pos != -1:
                         equals_pos = data.find(b"=", i, sep_pos)
                     else:
    -                    equals_pos = data.find(b"=", i)
    +                    equals_pos = data.find(b"=", i, length)
     
                     if equals_pos != -1:
                         # Emit this name.
    @@ -921,11 +918,8 @@ def _internal_write(self, data: bytes, length: int) -> int:
                             i = length
     
                 elif state == QuerystringState.FIELD_DATA:
    -                # Try finding either an ampersand or a semicolon after this
    -                # position.
    -                sep_pos = data.find(b"&", i)
    -                if sep_pos == -1:
    -                    sep_pos = data.find(b";", i)
    +                # Try finding an ampersand after this position.
    +                sep_pos = data.find(b"&", i, length)
     
                     # If we found it, callback this bit as data and then go back
                     # to expecting to find a field.
    
  • tests/test_multipart.py+3 3 modified
    @@ -428,10 +428,10 @@ def test_streaming_break(self) -> None:
             self.p.write(b"f=baz")
             self.assert_fields((b"asdf", b"baz"))
     
    -    def test_semicolon_separator(self) -> None:
    -        self.p.write(b"foo=bar;asdf=baz")
    +    def test_semicolon_is_data_not_a_field_separator(self) -> None:
    +        self.p.write(b"role=user&x=;role=admin")
     
    -        self.assert_fields((b"foo", b"bar"), (b"asdf", b"baz"))
    +        self.assert_fields((b"role", b"user"), (b"x", b";role=admin"))
     
         def test_too_large_field(self) -> None:
             self.p.max_size = 15
    

Vulnerability mechanics

Root cause

"Inefficient two-step separator lookup in `QuerystringParser` scans the entire remaining buffer for `&` on every field iteration, causing quadratic time complexity when the body uses `;` as the separator."

Attack vector

An attacker sends a crafted `application/x-www-form-urlencoded` body consisting of many semicolon-separated fields (e.g., `a;a;a;...`) with no `&` characters. Because the parser first scans the entire remaining buffer for `&` (which always fails) before scanning for `;`, each field iteration performs a full buffer scan, yielding O(B²) byte comparisons per chunk [CWE-400][CWE-407]. A small body (e.g., 1 MiB) can consume seconds of CPU per request, and a handful of concurrent requests can exhaust worker processes [ref_id=1][ref_id=2].

Affected code

The vulnerability is in `python_multipart/multipart.py` in the `QuerystringParser._internal_write` method, specifically in the `FIELD_NAME` and `FIELD_DATA` states. Both states used a two-step separator lookup: `sep_pos = data.find(b"&", i)` followed by `if sep_pos == -1: sep_pos = data.find(b";", i)`. This pattern caused a full buffer scan for `&` on every field iteration when the body used `;` as the separator.

What the fix does

The patch [patch_id=6110791] removes `;` as a recognized field separator, treating only `&` per the WHATWG URL standard. The `FIELD_NAME` and `FIELD_DATA` states now call `data.find(b"&", i, length)` with a bounded scan instead of scanning to the end of the buffer. This eliminates the failed full-buffer `&` scan on every field iteration, making parsing linear in the body length. The test is updated to confirm that `;` is treated as data rather than a separator.

Preconditions

  • configThe target must accept `application/x-www-form-urlencoded` request bodies via `QuerystringParser`, `FormParser`, `create_form_parser`, or `parse_form` APIs.
  • networkThe attacker must be able to send HTTP requests to the vulnerable endpoint (no authentication required).
  • inputThe crafted body must use `;` as the field separator and contain no `&` characters.

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

References

2

News mentions

0

No linked articles in our index yet.