Skip to content

fix core: accept empty parameter slots in multipart headers - #1358

Open
SSE4 wants to merge 1 commit into
userver-framework:developfrom
SSE4:multipart-empty-parameter-fix
Open

SSE4 wants to merge 1 commit into
userver-framework:developfrom
SSE4:multipart-empty-parameter-fix

Conversation

@SSE4

@SSE4 SSE4 commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

closes: #1357

A multipart/form-data request whose Content-Type contains an empty parameter
slot — a trailing ;, or ;; — is rejected with
400 invalid body of multipart/form-data request, even when the body is
perfectly valid. The same applies to a part's Content-Disposition. RFC 9110
§5.6.6 explicitly permits empty parameter slots.

Why

RFC 9110 §5.6.6, via
§8.3.1:

media-type = type "/" subtype parameters
parameters = *( OWS ";" OWS [ parameter ] )
parameter  = parameter-name "=" parameter-value

The [ parameter ] brackets make an empty slot valid, so
multipart/form-data; boundary=zzz; is a conforming Content-Type and userver
must accept it.

This is a change from the obsoleted specs: RFC 2068 §3.7, RFC 2616 §3.7 and
RFC 7231 §3.1.1.1 all had the parameter mandatory
(*( OWS ";" OWS parameter )), so under those a trailing ; really was a
grammar violation. RFC 9110 (June 2022) obsoletes all three.

There is also no sender-side prohibition to fall back on: §5.6.1.1's "a sender
MUST NOT generate empty list elements"
is scoped to the comma-list (#)
construct, not to parameters. A client sending this is not misbehaving.

How

ParseMultipartFormData and ParseContentDisposition each have a parameter loop
that calls ReadToken after consuming a ; and treats an empty token as fatal.
Both now treat it as a valid no-op, by inverting the existing
if (!param_name.empty()) into a guard clause:

auto param_name = ReadToken(unparsed);
if (param_name.empty()) {
    // `parameters` from https://www.rfc-editor.org/rfc/rfc9110#section-5.6.6
    // is `*( OWS ";" OWS [ parameter ] )`, so an empty parameter slot is
    // valid and carries no information
    continue;
}

The rest of the production diff is the resulting dedent of the former if body —
no logic change there, so ?w=1 collapses it to the four lines above, twice.

The RFC-citation comment follows the existing convention in this file, which
already annotates ReadToken and ReadHeaderValue with the grammar rule and
link they implement.

Termination is unaffected. SkipSymbol(str, ';') runs at the top of the loop
before ReadToken, so every iteration consumes at least one character, and any
non-; garbage still fails on that SkipSymbol.

What still fails

The change is deliberately narrow — an empty slot is accepted, nothing else:

input result
multipart/form-data; boundary=zzz; novalue still rejected — [ parameter ] is a complete name=value or nothing; a bare token is neither
Content-Disposition: form-data; name="a"; novalue still rejected, same reason
multipart/form-data;; still rejected — no boundary parameter
Content-Disposition: form-data; still rejected — no name parameter

Each of these is pinned by ParseInvalidParametersStillFail so the leniency
cannot widen unnoticed later.

Tests

Three tests in multipart_form_data_parser_test.cpp:

  • ParseEmptyContentTypeParameter — seven content types: trailing ;, ;;,
    ; ; with spaces, an empty slot in the middle, ;; directly after the media
    type, and with a charset parameter present. All expect the arg to parse.
  • ParseEmptyContentDispositionParameter — the same shapes in a part's
    Content-Disposition.
  • ParseInvalidParametersStillFail — the negative guard above.

Verified locally:

  • without the production change, the two positive tests fail on all 11
    sub-cases
    ; with it they pass
  • ParseInvalidParametersStillFail passes either way
  • the 19 pre-existing MultipartFormDataParser tests are unaffected

Impact

Requests that userver rejects today with
400 invalid body of multipart/form-data request and
Bad Content-Type: empty attribute name (or
Bad Content-Disposition: empty attribute name) now reach the handler with
their form arguments parsed. No currently-accepted input changes behaviour, and
no test asserted the old rejection.

RFC 9110 5.6.6 defines `parameters = *( OWS ";" OWS [ parameter ] )`, so an
empty parameter slot -- a trailing ';' or ';;' -- is valid and carries no
information. userver rejected such a Content-Type or Content-Disposition with
`400 invalid body of multipart/form-data request`, without even looking at the
request body.

The mandatory-parameter grammar userver implements comes from RFC 2616 3.7 and
RFC 7231 3.1.1.1, both obsoleted by RFC 9110 in June 2022.

A valueless parameter (a bare token in a parameter slot) remains an error, as
do a Content-Type without `boundary` and a Content-Disposition without `name`.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

400 on a Content-Type with an empty parameter (RFC 9110 §5.6.6)

1 participant