Skip to content

Count multipart preamble bytes against the parser length limit - #1336

Closed
neilberkman wants to merge 1 commit into
elixir-plug:mainfrom
neilberkman:multipart-preamble-length
Closed

neilberkman wants to merge 1 commit into
elixir-plug:mainfrom
neilberkman:multipart-preamble-length

Conversation

@neilberkman

Copy link
Copy Markdown
Contributor

Preamble bytes before the first --boundary delimiter are never charged to :length.

:plug_multipart.skip_preamble/2 discards each chunk down to its last three bytes before returning {:more, rest}, so read_part_headers_more/7 only ever compares those retained bytes against the limit. When the boundary is reached, Plug.Parsers.MULTIPART.parse_multipart_headers/5 subtracts only headers_size(headers) from its budget, so preamble bytes skipped in earlier chunks, or ahead of --boundary in the final chunk, are not counted either.

With default parser options (length: 8_000_000, read_length: 1_000_000) on both Bandit and Cowboy:

  • a 9,000,000-byte preamble followed by a 2-byte part returns 200
  • an 800,000-byte preamble followed by a 7,500,000-byte part (8.3 MB total) returns 200

With this change both raise Plug.Parsers.RequestTooLargeError (413).

Changes:

  • lib/plug/conn.ex: read_part_headers/8 carries the count of discarded preamble bytes, returns {:error, :too_large, conn} as soon as that count exceeds length, and stores the total for the part (including any prefix before --boundary in the final header chunk) in conn.private[:plug_multipart_skipped].
  • lib/plug/parsers/multipart.ex: parse_multipart_headers/5 subtracts the stored count from limit.
  • test/plug/parsers_test.exs: a test with an 800 KB preamble and a 7.5 MB part under default options expects RequestTooLargeError with status 413.

Full suite passes on a735b14 (690 passed). Preambles are rare from browsers, so the only behavioural change for ordinary clients is that a request whose preamble plus parts exceeds :length is now rejected.

Preamble bytes before the first boundary delimiter were never charged to
:length. :plug_multipart.skip_preamble/2 discards each chunk down to its
last three bytes before returning {:more, rest}, so read_part_headers_more/7
only ever compared the retained bytes against the limit, and
Plug.Parsers.MULTIPART subtracted only the header size from its budget.
A 9 MB preamble ahead of a 2-byte part passed the default 8 MB limit on
both Bandit and Cowboy.

read_part_headers/8 now carries the number of discarded preamble bytes,
returns {:error, :too_large, conn} as soon as they exceed the limit, and
records the total in conn.private[:plug_multipart_skipped] so the parser
subtracts it from its remaining budget.
@josevalim josevalim closed this in 5afec8c Oct 25, 2026
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.

1 participant