Skip to content

Commit d878b42

Browse files
committed
Respect length when decoding multipart headers
1 parent a447199 commit d878b42

4 files changed

Lines changed: 114 additions & 35 deletions

File tree

lib/plug/conn.ex

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1196,8 +1196,10 @@ defmodule Plug.Conn do
11961196
@doc """
11971197
Reads the headers of a multipart request.
11981198
1199-
It returns `{:ok, headers, conn}` with the headers or
1200-
`{:done, conn}` if there are no more parts.
1199+
It returns `{:ok, headers, conn}` with the headers,
1200+
`{:error, :too_large, conn}` if the current multipart header block
1201+
exceeds the configured `:length`, or `{:done, conn}` if there are
1202+
no more parts.
12011203
12021204
Once `read_part_headers/2` is invoked, you may call
12031205
`read_part_body/2` to read the body associated to the headers.
@@ -1206,40 +1208,42 @@ defmodule Plug.Conn do
12061208
12071209
## Options
12081210
1209-
* `:length` - sets the maximum number of bytes to read from the body for
1210-
each chunk, defaults to `64_000` bytes
1211+
* `:length` - sets the maximum number of bytes to read while parsing the
1212+
current multipart header block, defaults to `64_000` bytes
12111213
* `:read_length` - sets the amount of bytes to read at one time from the
12121214
underlying socket to fill the chunk, defaults to `64_000` bytes
12131215
* `:read_timeout` - sets the timeout for each socket read, defaults to
12141216
`5_000` milliseconds
12151217
12161218
"""
1217-
@spec read_part_headers(t, Keyword.t()) :: {:ok, headers, t} | {:done, t}
1219+
@spec read_part_headers(t, Keyword.t()) ::
1220+
{:ok, headers, t} | {:error, :too_large, t} | {:done, t}
12181221
def read_part_headers(%Conn{adapter: {adapter, state}} = conn, opts \\ []) do
1219-
opts = opts ++ [length: 64_000, read_length: 64_000, read_timeout: 5000]
1220-
12211222
case init_multipart(conn) do
12221223
{boundary, buffer} ->
1224+
opts = opts ++ [length: 64_000, read_length: 64_000, read_timeout: 5000]
1225+
length = Keyword.fetch!(opts, :length)
12231226
{data, state} = read_multipart_from_buffer_or_adapter(buffer, adapter, state, opts)
1224-
read_part_headers(conn, data, boundary, adapter, state, opts)
1227+
read_part_headers(conn, data, length, boundary, adapter, state, opts)
12251228

12261229
:done ->
12271230
{:done, conn}
12281231
end
12291232
end
12301233

1231-
defp read_part_headers(conn, data, boundary, adapter, state, opts) do
1234+
defp read_part_headers(conn, data, length, boundary, adapter, state, opts) do
12321235
case :plug_multipart.parse_headers(data, boundary) do
1236+
{:ok, _headers, rest} when byte_size(data) - byte_size(rest) > length ->
1237+
{:error, :too_large, store_multipart(conn, {boundary, data}, adapter, state)}
1238+
12331239
{:ok, headers, rest} ->
12341240
{:ok, headers, store_multipart(conn, {boundary, rest}, adapter, state)}
12351241

12361242
:more ->
1237-
{_, next, state} = next_multipart(adapter, state, opts)
1238-
read_part_headers(conn, data <> next, boundary, adapter, state, opts)
1243+
read_part_headers_more(conn, data, length, boundary, adapter, state, opts)
12391244

12401245
{:more, rest} ->
1241-
{_, next, state} = next_multipart(adapter, state, opts)
1242-
read_part_headers(conn, rest <> next, boundary, adapter, state, opts)
1246+
read_part_headers_more(conn, rest, length, boundary, adapter, state, opts)
12431247

12441248
{:done, _} ->
12451249
{:done, store_multipart(conn, :done, adapter, state)}
@@ -1328,6 +1332,16 @@ defmodule Plug.Conn do
13281332
%{put_in(conn.private[:plug_multipart], multipart) | adapter: {adapter, state}}
13291333
end
13301334

1335+
defp read_part_headers_more(conn, data, length, boundary, adapter, state, _opts)
1336+
when byte_size(data) >= length do
1337+
{:error, :too_large, store_multipart(conn, {boundary, data}, adapter, state)}
1338+
end
1339+
1340+
defp read_part_headers_more(conn, data, length, boundary, adapter, state, opts) do
1341+
{_, next, state} = next_multipart(adapter, state, opts)
1342+
read_part_headers(conn, data <> next, length, boundary, adapter, state, opts)
1343+
end
1344+
13311345
defp read_multipart_from_buffer_or_adapter("", adapter, state, opts) do
13321346
{_, data, state} = adapter.read_req_body(state, opts)
13331347
{data, state}

lib/plug/parsers/multipart.ex

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,6 @@ defmodule Plug.Parsers.MULTIPART do
2222
Besides the options supported by `Plug.Conn.read_body/2`, the multipart parser
2323
also checks for:
2424
25-
* `:headers` - containing the same `:length`, `:read_length`
26-
and `:read_timeout` options which are used explicitly for parsing multipart
27-
headers
28-
2925
* `:validate_utf8` - specifies whether multipart body parts should be validated
3026
as utf8 binaries. It is either a boolean or a custom exception to raise
3127
@@ -102,15 +98,12 @@ defmodule Plug.Parsers.MULTIPART do
10298
{read_length, opts} = Keyword.pop(opts, :read_length, 1_000_000)
10399
opts = [length: read_length, read_length: read_length] ++ opts
104100

105-
# The header options are handled individually.
106-
{headers_opts, opts} = Keyword.pop(opts, :headers, [])
107-
108101
unless is_integer(limit) do
109102
raise ":length option for Plug.Parsers.MULTIPART must be an integer"
110103
end
111104

112105
m2p = opts[:multipart_to_params] || {__MODULE__, :multipart_to_params, [opts]}
113-
{m2p, limit, headers_opts, opts}
106+
{m2p, limit, opts}
114107
end
115108

116109
@impl true
@@ -120,7 +113,7 @@ defmodule Plug.Parsers.MULTIPART do
120113
parse_multipart(conn, opts_tuple)
121114
rescue
122115
# Do not ignore upload errors
123-
e in [Plug.UploadError, Plug.Parsers.BadEncodingError] ->
116+
e in [Plug.UploadError, Plug.Parsers.BadEncodingError, Plug.Parsers.RequestTooLargeError] ->
124117
reraise e, __STACKTRACE__
125118

126119
# All others are wrapped
@@ -146,29 +139,43 @@ defmodule Plug.Parsers.MULTIPART do
146139

147140
## Multipart
148141

149-
defp parse_multipart(conn, {m2p, limit, headers_opts, opts}) do
150-
read_result = Plug.Conn.read_part_headers(conn, headers_opts)
151-
{:ok, limit, acc, conn} = parse_multipart(read_result, limit, opts, headers_opts, [])
142+
defp parse_multipart(conn, {m2p, limit, opts}) do
143+
read_result = read_part_headers(conn, limit, opts)
152144

153-
if limit > 0 do
154-
{mod, fun, args} = m2p
155-
apply(mod, fun, [acc, conn | args])
156-
else
157-
{:error, :too_large, conn}
145+
case parse_multipart(read_result, limit, opts, []) do
146+
{:ok, limit, acc, conn} ->
147+
if limit >= 0 do
148+
{mod, fun, args} = m2p
149+
apply(mod, fun, [acc, conn | args])
150+
else
151+
{:error, :too_large, conn}
152+
end
153+
154+
{:error, :too_large, conn} ->
155+
{:error, :too_large, conn}
158156
end
159157
end
160158

161-
defp parse_multipart({:ok, headers, conn}, limit, opts, headers_opts, acc) when limit >= 0 do
159+
defp parse_multipart({:ok, headers, conn}, limit, opts, acc) when limit >= 0 do
162160
{conn, limit, acc} = parse_multipart_headers(headers, conn, limit, opts, acc)
163-
read_result = Plug.Conn.read_part_headers(conn, headers_opts)
164-
parse_multipart(read_result, limit, opts, headers_opts, acc)
161+
162+
if limit >= 0 do
163+
read_result = read_part_headers(conn, limit, opts)
164+
parse_multipart(read_result, limit, opts, acc)
165+
else
166+
{:ok, limit, acc, conn}
167+
end
168+
end
169+
170+
defp parse_multipart({:error, :too_large, conn}, _limit, _opts, _acc) do
171+
{:error, :too_large, conn}
165172
end
166173

167-
defp parse_multipart({:ok, _headers, conn}, limit, _opts, _headers_opts, acc) do
174+
defp parse_multipart({:ok, _headers, conn}, limit, _opts, acc) do
168175
{:ok, limit, acc, conn}
169176
end
170177

171-
defp parse_multipart({:done, conn}, limit, _opts, _headers_opts, acc) do
178+
defp parse_multipart({:done, conn}, limit, _opts, acc) do
172179
{:ok, limit, acc, conn}
173180
end
174181

@@ -296,4 +303,9 @@ defmodule Plug.Parsers.MULTIPART do
296303
nil -> nil
297304
end
298305
end
306+
307+
defp read_part_headers(conn, limit, opts) do
308+
headers_length = min(limit, Keyword.fetch!(opts, :length))
309+
Plug.Conn.read_part_headers(conn, Keyword.put(opts, :length, headers_length))
310+
end
299311
end

test/plug/conn_test.exs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -902,6 +902,40 @@ defmodule Plug.ConnTest do
902902
assert {:more, _, _} = read_body(conn, length: 100)
903903
end
904904

905+
test "read_part_headers/2 returns too_large while accumulating multipart headers" do
906+
body =
907+
[
908+
"--deadbeef\r\ncontent-disposition: form-data; name=\"x\"\r\ncontent-type: ",
909+
String.duplicate("a", 2_000),
910+
"\r\n\r\nabc\r\n--deadbeef--\r\n"
911+
]
912+
|> IO.iodata_to_binary()
913+
914+
conn =
915+
conn(:post, "/", body)
916+
|> put_req_header("content-type", "multipart/form-data; boundary=deadbeef")
917+
918+
assert {:error, :too_large, _conn} = read_part_headers(conn, length: 1_000)
919+
end
920+
921+
test "read_part_headers/2 returns too_large for buffered multipart headers over the limit" do
922+
buffer =
923+
[
924+
"--deadbeef\r\ncontent-disposition: form-data; name=\"x\"\r\ncontent-type: ",
925+
String.duplicate("a", 2_000),
926+
"\r\n\r\nabc\r\n--deadbeef--\r\n"
927+
]
928+
|> IO.iodata_to_binary()
929+
930+
conn =
931+
conn(:post, "/", "")
932+
|> put_req_header("content-type", "multipart/form-data; boundary=deadbeef")
933+
934+
conn = %{conn | private: Map.put(conn.private, :plug_multipart, {"deadbeef", buffer})}
935+
936+
assert {:error, :too_large, _conn} = read_part_headers(conn, length: 1_000)
937+
end
938+
905939
test "query_params/1 and fetch_query_params/1" do
906940
conn = conn(:get, "/foo?a=b&c=d")
907941
assert conn.query_params == %Plug.Conn.Unfetched{aspect: :query_params}

test/plug/parsers_test.exs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,25 @@ defmodule Plug.ParsersTest do
389389
assert Plug.Exception.status(exception) == 413
390390
end
391391

392+
test "raises on multipart headers larger than the parser length" do
393+
multipart =
394+
[
395+
"--deadbeef\r\ncontent-disposition: form-data; name=\"x\"\r\ncontent-type: ",
396+
String.duplicate("a", 2_000),
397+
"\r\n\r\nabc\r\n--deadbeef--\r\n"
398+
]
399+
|> IO.iodata_to_binary()
400+
401+
exception =
402+
assert_raise Plug.Parsers.RequestTooLargeError, ~r/the request is too large/, fn ->
403+
conn(:post, "/", multipart)
404+
|> put_req_header("content-type", "multipart/form-data; boundary=deadbeef")
405+
|> parse(length: 1_000)
406+
end
407+
408+
assert Plug.Exception.status(exception) == 413
409+
end
410+
392411
test "raises when request cannot be processed" do
393412
message = "unsupported media type text/plain"
394413

0 commit comments

Comments
 (0)