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

Starlette: request.form() limits silently ignored for application/x-www-form-urlencoded enable DoS

CVE-2026-54283

Description

request.form() in Starlette/FastAPI silently ignores max_fields and max_part_size for url-encoded bodies, enabling denial of service via CPU blocking or memory exhaustion.

AI Insight

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

request.form() in Starlette/FastAPI silently ignores max_fields and max_part_size for url-encoded bodies, enabling denial of service via CPU blocking or memory exhaustion.

Vulnerability

The request.form() method in Starlette (and FastAPI which uses it) accepts max_fields and max_part_size parameters to limit resource consumption. However, these limits are only enforced for multipart/form-data requests; for application/x-www-form-urlencoded, the parser is constructed without these limits, causing them to be silently ignored [1][2]. This occurs in all Starlette versions prior to the fix.

Exploitation

An unauthenticated attacker can send a crafted application/x-www-form-urlencoded request with either an extremely large number of fields (e.g., 1,000,000 fields in a sub-10MB payload) or a single field with a very large value (e.g., 50MB) [1]. The request synchronously blocks the event loop during parsing, causing the worker to be unresponsive.

Impact

Successful exploitation leads to denial of service (DoS). A large number of fields causes CPU exhaustion and event-loop blocking for several seconds, while a large field value forces unbounded memory allocation, potentially crashing the server [2]. Parallel requests can exacerbate the impact.

Mitigation

The vulnerability is fixed in Starlette version [insert fixed version] as per the GitHub advisory [1]. Users should upgrade to the latest patched version. If an immediate upgrade is not possible, avoid calling request.form() on untrusted application/x-www-form-urlencoded input, or implement external rate limiting and body size restrictions at the reverse proxy.

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

2
25b8e179d8d7

Enforce `FormParser` limits in parser callbacks (#3331)

https://github.com/kludex/starletteMarcelo TrylesinskiJun 12, 2026Fixed in 1.3.1via llm-release-walk
2 files changed · +39 11
  • starlette/formparsers.py+12 8 modified
    @@ -69,20 +69,32 @@ def __init__(
             self.max_fields = max_fields
             self.max_part_size = max_part_size
             self.messages: list[tuple[FormMessage, bytes]] = []
    +        self._current_field_size = 0
    +        self._current_fields = 0
     
         def on_field_start(self) -> None:
    +        self._current_field_size = 0
             message = (FormMessage.FIELD_START, b"")
             self.messages.append(message)
     
         def on_field_name(self, data: bytes, start: int, end: int) -> None:
    +        self._current_field_size += end - start
    +        if self._current_field_size > self.max_part_size:
    +            raise MultiPartException(f"Field exceeded maximum size of {int(self.max_part_size / 1024)}KB.")
             message = (FormMessage.FIELD_NAME, data[start:end])
             self.messages.append(message)
     
         def on_field_data(self, data: bytes, start: int, end: int) -> None:
    +        self._current_field_size += end - start
    +        if self._current_field_size > self.max_part_size:
    +            raise MultiPartException(f"Field exceeded maximum size of {int(self.max_part_size / 1024)}KB.")
             message = (FormMessage.FIELD_DATA, data[start:end])
             self.messages.append(message)
     
         def on_field_end(self) -> None:
    +        self._current_fields += 1
    +        if self._current_fields > self.max_fields:
    +            raise MultiPartException(f"Too many fields. Maximum number of fields is {self.max_fields}.")
             message = (FormMessage.FIELD_END, b"")
             self.messages.append(message)
     
    @@ -106,7 +118,6 @@ async def parse(self) -> FormData:
             field_value = bytearray()
     
             items: list[tuple[str, str | UploadFile]] = []
    -        field_count = 0
     
             # Feed the parser with data from the request.
             async for chunk in self.stream:
    @@ -121,17 +132,10 @@ async def parse(self) -> FormData:
                         field_name = bytearray()
                         field_value = bytearray()
                     elif message_type == FormMessage.FIELD_NAME:
    -                    if len(field_name) + len(field_value) + len(message_bytes) > self.max_part_size:
    -                        raise MultiPartException(f"Field exceeded maximum size of {int(self.max_part_size / 1024)}KB.")
                         field_name.extend(message_bytes)
                     elif message_type == FormMessage.FIELD_DATA:
    -                    if len(field_name) + len(field_value) + len(message_bytes) > self.max_part_size:
    -                        raise MultiPartException(f"Field exceeded maximum size of {int(self.max_part_size / 1024)}KB.")
                         field_value.extend(message_bytes)
                     elif message_type == FormMessage.FIELD_END:
    -                    field_count += 1
    -                    if field_count > self.max_fields:
    -                        raise MultiPartException(f"Too many fields. Maximum number of fields is {self.max_fields}.")
                         name = unquote_plus(field_name.decode("latin-1"))
                         value = unquote_plus(field_value.decode("latin-1"))
                         items.append((name, value))
    
  • tests/test_formparsers.py+27 3 modified
    @@ -2,7 +2,7 @@
     
     import os
     import threading
    -from collections.abc import Generator
    +from collections.abc import AsyncGenerator, Generator
     from contextlib import AbstractContextManager, nullcontext as does_not_raise
     from io import BytesIO
     from pathlib import Path
    @@ -13,8 +13,8 @@
     import pytest
     
     from starlette.applications import Starlette
    -from starlette.datastructures import UploadFile
    -from starlette.formparsers import MultiPartException, MultiPartParser, _user_safe_decode
    +from starlette.datastructures import Headers, UploadFile
    +from starlette.formparsers import FormParser, MultiPartException, MultiPartParser, _user_safe_decode
     from starlette.requests import Request
     from starlette.responses import JSONResponse
     from starlette.routing import Mount
    @@ -589,6 +589,30 @@ def test_urlencoded_max_part_size_is_customizable(
             assert res.text == "Field exceeded maximum size of 10KB."
     
     
    +@pytest.mark.anyio
    +async def test_urlencoded_limits_stop_parsing_within_a_single_chunk() -> None:
    +    async def single_chunk(body: bytes) -> AsyncGenerator[bytes, None]:
    +        yield body
    +
    +    headers = Headers({"content-type": "application/x-www-form-urlencoded"})
    +
    +    too_many = "&".join(f"f{i}=" for i in range(100_000)).encode()
    +    stream = single_chunk(too_many)
    +    parser = FormParser(headers, stream, max_fields=10)
    +    with pytest.raises(MultiPartException, match="Too many fields"):
    +        await parser.parse()
    +    await stream.aclose()
    +    assert parser._current_fields == 11
    +
    +    too_big = ("field=" + "x" * (1024 * 1024 * 50)).encode()
    +    stream = single_chunk(too_big)
    +    parser = FormParser(headers, stream, max_part_size=1024)
    +    with pytest.raises(MultiPartException, match="Field exceeded maximum size"):
    +        await parser.parse()
    +    await stream.aclose()
    +    assert sum(len(data) for _, data in parser.messages) <= 1024
    +
    +
     def test_user_safe_decode_helper() -> None:
         result = _user_safe_decode(b"\xc4\x99\xc5\xbc\xc4\x87", "utf-8")
         assert result == "ężć"
    
dba1c4babc4f

Enforce `max_fields` and `max_part_size` in `FormParser` (#3329)

https://github.com/kludex/starletteMarcelo TrylesinskiJun 12, 2026Fixed in 1.3.1via llm-release-walk
3 files changed · +154 3
  • starlette/formparsers.py+18 1 modified
    @@ -55,10 +55,19 @@ def __init__(self, message: str) -> None:
     
     
     class FormParser:
    -    def __init__(self, headers: Headers, stream: AsyncGenerator[bytes, None]) -> None:
    +    def __init__(
    +        self,
    +        headers: Headers,
    +        stream: AsyncGenerator[bytes, None],
    +        *,
    +        max_fields: int | float = 1000,
    +        max_part_size: int = 1024 * 1024,  # 1MB
    +    ) -> None:
             assert multipart is not None, "The `python-multipart` library must be installed to use form parsing."
             self.headers = headers
             self.stream = stream
    +        self.max_fields = max_fields
    +        self.max_part_size = max_part_size
             self.messages: list[tuple[FormMessage, bytes]] = []
     
         def on_field_start(self) -> None:
    @@ -97,6 +106,7 @@ async def parse(self) -> FormData:
             field_value = bytearray()
     
             items: list[tuple[str, str | UploadFile]] = []
    +        field_count = 0
     
             # Feed the parser with data from the request.
             async for chunk in self.stream:
    @@ -111,10 +121,17 @@ async def parse(self) -> FormData:
                         field_name = bytearray()
                         field_value = bytearray()
                     elif message_type == FormMessage.FIELD_NAME:
    +                    if len(field_name) + len(field_value) + len(message_bytes) > self.max_part_size:
    +                        raise MultiPartException(f"Field exceeded maximum size of {int(self.max_part_size / 1024)}KB.")
                         field_name.extend(message_bytes)
                     elif message_type == FormMessage.FIELD_DATA:
    +                    if len(field_name) + len(field_value) + len(message_bytes) > self.max_part_size:
    +                        raise MultiPartException(f"Field exceeded maximum size of {int(self.max_part_size / 1024)}KB.")
                         field_value.extend(message_bytes)
                     elif message_type == FormMessage.FIELD_END:
    +                    field_count += 1
    +                    if field_count > self.max_fields:
    +                        raise MultiPartException(f"Too many fields. Maximum number of fields is {self.max_fields}.")
                         name = unquote_plus(field_name.decode("latin-1"))
                         value = unquote_plus(field_value.decode("latin-1"))
                         items.append((name, value))
    
  • starlette/requests.py+12 2 modified
    @@ -294,8 +294,18 @@ async def _get_form(
                             raise HTTPException(status_code=400, detail=exc.message)
                         raise exc
                 elif content_type == b"application/x-www-form-urlencoded":
    -                form_parser = FormParser(self.headers, self.stream())
    -                self._form = await form_parser.parse()
    +                try:
    +                    form_parser = FormParser(
    +                        self.headers,
    +                        self.stream(),
    +                        max_fields=max_fields,
    +                        max_part_size=max_part_size,
    +                    )
    +                    self._form = await form_parser.parse()
    +                except MultiPartException as exc:
    +                    if "app" in self.scope:
    +                        raise HTTPException(status_code=400, detail=exc.message)
    +                    raise exc
                 else:
                     self._form = FormData()
             return self._form
    
  • tests/test_formparsers.py+124 0 modified
    @@ -465,6 +465,130 @@ def test_multipart_multi_field_app_reads_body(tmpdir: Path, test_client_factory:
         assert response.json() == {"some": "data", "second": "key pair"}
     
     
    +@pytest.mark.parametrize(
    +    "app,expectation",
    +    [
    +        (app, pytest.raises(MultiPartException)),
    +        (Starlette(routes=[Mount("/", app=app)]), does_not_raise()),
    +    ],
    +)
    +def test_urlencoded_too_many_fields_raise(
    +    app: ASGIApp,
    +    expectation: AbstractContextManager[Exception],
    +    test_client_factory: TestClientFactory,
    +) -> None:
    +    client = test_client_factory(app)
    +    data = "&".join(f"N{i}=" for i in range(1001))
    +    with expectation:
    +        res = client.post(
    +            "/",
    +            content=data,
    +            headers={"Content-Type": "application/x-www-form-urlencoded"},
    +        )
    +        assert res.status_code == 400
    +        assert res.text == "Too many fields. Maximum number of fields is 1000."
    +
    +
    +@pytest.mark.parametrize(
    +    "app,expectation",
    +    [
    +        (app, pytest.raises(MultiPartException)),
    +        (Starlette(routes=[Mount("/", app=app)]), does_not_raise()),
    +    ],
    +)
    +def test_urlencoded_field_exceeds_max_part_size_raise(
    +    app: ASGIApp,
    +    expectation: AbstractContextManager[Exception],
    +    test_client_factory: TestClientFactory,
    +) -> None:
    +    client = test_client_factory(app)
    +    data = "field=" + "x" * (1024 * 1024 + 1)
    +    with expectation:
    +        res = client.post(
    +            "/",
    +            content=data,
    +            headers={"Content-Type": "application/x-www-form-urlencoded"},
    +        )
    +        assert res.status_code == 400
    +        assert res.text == "Field exceeded maximum size of 1024KB."
    +
    +
    +@pytest.mark.parametrize(
    +    "app,expectation",
    +    [
    +        (app, pytest.raises(MultiPartException)),
    +        (Starlette(routes=[Mount("/", app=app)]), does_not_raise()),
    +    ],
    +)
    +def test_urlencoded_field_name_exceeds_max_part_size_raise(
    +    app: ASGIApp,
    +    expectation: AbstractContextManager[Exception],
    +    test_client_factory: TestClientFactory,
    +) -> None:
    +    client = test_client_factory(app)
    +    data = "x" * (1024 * 1024 + 1) + "=value"
    +    with expectation:
    +        res = client.post(
    +            "/",
    +            content=data,
    +            headers={"Content-Type": "application/x-www-form-urlencoded"},
    +        )
    +        assert res.status_code == 400
    +        assert res.text == "Field exceeded maximum size of 1024KB."
    +
    +
    +@pytest.mark.parametrize(
    +    "app,expectation",
    +    [
    +        (make_app_max_parts(max_fields=1), pytest.raises(MultiPartException)),
    +        (
    +            Starlette(routes=[Mount("/", app=make_app_max_parts(max_fields=1))]),
    +            does_not_raise(),
    +        ),
    +    ],
    +)
    +def test_urlencoded_max_fields_is_customizable(
    +    app: ASGIApp,
    +    expectation: AbstractContextManager[Exception],
    +    test_client_factory: TestClientFactory,
    +) -> None:
    +    client = test_client_factory(app)
    +    with expectation:
    +        res = client.post(
    +            "/",
    +            content="a=1&b=2",
    +            headers={"Content-Type": "application/x-www-form-urlencoded"},
    +        )
    +        assert res.status_code == 400
    +        assert res.text == "Too many fields. Maximum number of fields is 1."
    +
    +
    +@pytest.mark.parametrize(
    +    "app,expectation",
    +    [
    +        (make_app_max_parts(max_part_size=1024 * 10), pytest.raises(MultiPartException)),
    +        (
    +            Starlette(routes=[Mount("/", app=make_app_max_parts(max_part_size=1024 * 10))]),
    +            does_not_raise(),
    +        ),
    +    ],
    +)
    +def test_urlencoded_max_part_size_is_customizable(
    +    app: ASGIApp,
    +    expectation: AbstractContextManager[Exception],
    +    test_client_factory: TestClientFactory,
    +) -> None:
    +    client = test_client_factory(app)
    +    with expectation:
    +        res = client.post(
    +            "/",
    +            content="field=" + "x" * (1024 * 10 + 1),
    +            headers={"Content-Type": "application/x-www-form-urlencoded"},
    +        )
    +        assert res.status_code == 400
    +        assert res.text == "Field exceeded maximum size of 10KB."
    +
    +
     def test_user_safe_decode_helper() -> None:
         result = _user_safe_decode(b"\xc4\x99\xc5\xbc\xc4\x87", "utf-8")
         assert result == "ężć"
    

Vulnerability mechanics

Root cause

"The `FormParser` for `application/x-www-form-urlencoded` was constructed without the `max_fields` and `max_part_size` parameters, so the configured resource limits were silently ignored for that content type."

Attack vector

An unauthenticated attacker sends an HTTP POST request with `Content-Type: application/x-www-form-urlencoded` to an endpoint that calls `request.form()`. Because the url-encoded parser was constructed without the `max_fields` and `max_part_size` limits that the application developer configured, the attacker can include an arbitrarily large number of fields (e.g. 1,000,000 fields in a sub-10MB payload) to block the worker's event loop for several seconds, or a single oversized field value (e.g. 50MB) to force unbounded memory allocation [CWE-770]. The same request sent as `multipart/form-data` would be correctly rejected with a `400` status.

What the fix does

The fix consists of two commits. [patch_id=6110788] adds `max_fields` and `max_part_size` parameters to `FormParser.__init__` and forwards them from `Request._get_form` when constructing the parser for `application/x-www-form-urlencoded` bodies. It also inserts size checks in the `parse()` loop that raise `MultiPartException` when a field name or value exceeds `max_part_size` or when the field count exceeds `max_fields`. [patch_id=6110787] moves those checks into the callback methods (`on_field_name`, `on_field_data`, `on_field_end`) so they fire even when the entire body arrives in a single stream chunk, preventing the parser from accumulating oversized data before the check runs. Together the patches ensure the configured limits are enforced identically for both content types.

Preconditions

  • configThe application must call request.form() on an incoming request with Content-Type: application/x-www-form-urlencoded.
  • authNo authentication is required; the attacker can send requests directly to the service.
  • networkThe attacker must be able to send HTTP POST requests to the target endpoint.
  • inputThe attacker controls the request body, which can contain a large number of fields or a single oversized field value.

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.