Skip to content

Commit

Permalink
HTTP API: limit default body size for binding creation
Browse files Browse the repository at this point in the history
It does not need to use the "worst case scenario"
default HTTP request body size limit that
is primarily necessary because definition imports
can be large (MiBs in size, for example).

Since exchange, queue names and routing key
have limits of 255 bytes and optional arguments
can practically be expected to be short, we
can lower the limit to < 10 KiB.
  • Loading branch information
michaelklishin committed Nov 9, 2024
1 parent 444df00 commit fb300d2
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 38 deletions.
33 changes: 30 additions & 3 deletions deps/rabbitmq_management/src/rabbit_mgmt_util.erl
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
list_login_vhosts_names/2]).
-export([filter_tracked_conn_list/3]).
-export([with_decode/5, decode/1, decode/2, set_resp_header/3,
args/1, read_complete_body/1]).
args/1, read_complete_body/1, read_complete_body_with_limit/2]).
-export([reply_list/3, reply_list/5, reply_list/4,
sort_list/2, destination_type/1, reply_list_or_paginate/3
]).
Expand Down Expand Up @@ -703,22 +703,49 @@ halt_response(Code, Type, Reason, ReqData, Context) ->
id(Key, ReqData) ->
rabbit_web_dispatch_access_control:id(Key, ReqData).

%% IMPORTANT:
%% Prefer read_complete_body_with_limit/2 with an explicit limit to make it easier
%% to reason about what limit will be used.
read_complete_body(Req) ->
read_complete_body(Req, <<"">>).
read_complete_body(Req, Acc) ->
BodySizeLimit = application:get_env(rabbitmq_management, max_http_body_size, ?MANAGEMENT_DEFAULT_HTTP_MAX_BODY_SIZE),
read_complete_body(Req, Acc, BodySizeLimit).
read_complete_body(Req0, Acc, BodySizeLimit) ->
case bit_size(Acc) > BodySizeLimit of
case byte_size(Acc) > BodySizeLimit of
true ->
{error, "Exceeded HTTP request body size limit"};
{error, http_body_limit_exceeded};
false ->
case cowboy_req:read_body(Req0) of
{ok, Data, Req} -> {ok, <<Acc/binary, Data/binary>>, Req};
{more, Data, Req} -> read_complete_body(Req, <<Acc/binary, Data/binary>>)
end
end.

read_complete_body_with_limit(Req, BodySizeLimit) when is_integer(BodySizeLimit) ->
case cowboy_req:body_length(Req) of
N when is_integer(N) ->
case N > BodySizeLimit of
true ->
{error, http_body_limit_exceeded, BodySizeLimit, N};
false ->
do_read_complete_body_with_limit(Req, <<"">>, BodySizeLimit)
end;
undefined ->
do_read_complete_body_with_limit(Req, <<"">>, BodySizeLimit)
end.

do_read_complete_body_with_limit(Req0, Acc, BodySizeLimit) ->
case byte_size(Acc) > BodySizeLimit of
true ->
{error, http_body_limit_exceeded, BodySizeLimit, byte_size(Acc)};
false ->
case cowboy_req:read_body(Req0, #{length => BodySizeLimit, period => 30000}) of
{ok, Data, Req} -> {ok, <<Acc/binary, Data/binary>>, Req};
{more, Data, Req} -> do_read_complete_body_with_limit(Req, <<Acc/binary, Data/binary>>, BodySizeLimit)
end
end.

with_decode(Keys, ReqData, Context, Fun) ->
case read_complete_body(ReqData) of
{error, Reason} ->
Expand Down
75 changes: 42 additions & 33 deletions deps/rabbitmq_management/src/rabbit_mgmt_wm_bindings.erl
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl").
-include_lib("amqp_client/include/amqp_client.hrl").

%% Use a much lower limit for creating bindings over the HTTP API.
%% The payload is not meant to be even 50 KiB in size.
-define(HTTP_BODY_SIZE_LIMIT, 5000).

%%--------------------------------------------------------------------

init(Req, [Mode]) ->
Expand Down Expand Up @@ -64,39 +68,44 @@ to_json(ReqData, {Mode, Context}) ->
ReqData, {Mode, Context}).

accept_content(ReqData0, {_Mode, Context}) ->
{ok, Body, ReqData} = rabbit_mgmt_util:read_complete_body(ReqData0),
Source = rabbit_mgmt_util:id(source, ReqData),
Dest = rabbit_mgmt_util:id(destination, ReqData),
DestType = rabbit_mgmt_util:id(dtype, ReqData),
VHost = rabbit_mgmt_util:vhost(ReqData),
{ok, Props} = rabbit_mgmt_util:decode(Body),
MethodName = case rabbit_mgmt_util:destination_type(ReqData) of
exchange -> 'exchange.bind';
queue -> 'queue.bind'
end,
{Key, Args} = key_args(DestType, Props),
case rabbit_mgmt_util:direct_request(
MethodName,
fun rabbit_mgmt_format:format_accept_content/1,
[{queue, Dest},
{exchange, Source},
{destination, Dest},
{source, Source},
{routing_key, Key},
{arguments, Args}],
"Binding error: ~ts", ReqData, Context) of
{stop, _, _} = Res ->
Res;
{true, ReqData, Context2} ->
From = binary_to_list(cowboy_req:path(ReqData)),
Prefix = rabbit_mgmt_util:get_path_prefix(),
BindingProps = rabbit_mgmt_format:pack_binding_props(Key, Args),
UrlWithBindings = rabbit_mgmt_format:url("/api/bindings/~ts/e/~ts/~ts/~ts/~ts",
[VHost, Source, DestType,
Dest, BindingProps]),
To = Prefix ++ binary_to_list(UrlWithBindings),
Loc = rabbit_web_dispatch_util:relativise(From, To),
{{true, Loc}, ReqData, Context2}
case rabbit_mgmt_util:read_complete_body_with_limit(ReqData0, ?HTTP_BODY_SIZE_LIMIT) of
{ok, Body, ReqData} ->
Source = rabbit_mgmt_util:id(source, ReqData),
Dest = rabbit_mgmt_util:id(destination, ReqData),
DestType = rabbit_mgmt_util:id(dtype, ReqData),
VHost = rabbit_mgmt_util:vhost(ReqData),
{ok, Props} = rabbit_mgmt_util:decode(Body),
MethodName = case rabbit_mgmt_util:destination_type(ReqData) of
exchange -> 'exchange.bind';
queue -> 'queue.bind'
end,
{Key, Args} = key_args(DestType, Props),
case rabbit_mgmt_util:direct_request(
MethodName,
fun rabbit_mgmt_format:format_accept_content/1,
[{queue, Dest},
{exchange, Source},
{destination, Dest},
{source, Source},
{routing_key, Key},
{arguments, Args}],
"Binding error: ~ts", ReqData, Context) of
{stop, _, _} = Res ->
Res;
{true, ReqData, Context2} ->
From = binary_to_list(cowboy_req:path(ReqData)),
Prefix = rabbit_mgmt_util:get_path_prefix(),
BindingProps = rabbit_mgmt_format:pack_binding_props(Key, Args),
UrlWithBindings = rabbit_mgmt_format:url("/api/bindings/~ts/e/~ts/~ts/~ts/~ts",
[VHost, Source, DestType,
Dest, BindingProps]),
To = Prefix ++ binary_to_list(UrlWithBindings),
Loc = rabbit_web_dispatch_util:relativise(From, To),
{{true, Loc}, ReqData, Context2}
end;
{error, http_body_limit_exceeded, LimitApplied, BytesRead} ->
rabbit_log:warning("HTTP API: binding creation request exceeded maximum allowed payload size (limit: ~tp bytes, payload size: ~tp bytes)", [LimitApplied, BytesRead]),
rabbit_mgmt_util:bad_request("Payload size limit exceeded", ReqData0, Context)
end.

is_authorized(ReqData, {Mode, Context}) ->
Expand Down
4 changes: 2 additions & 2 deletions deps/rabbitmq_management/src/rabbit_mgmt_wm_definitions.erl
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,9 @@ all_definitions(ReqData, Context) ->
Context).

accept_json(ReqData0, Context) ->
case rabbit_mgmt_util:read_complete_body(ReqData0) of
BodySizeLimit = application:get_env(rabbitmq_management, max_http_body_size, ?MANAGEMENT_DEFAULT_HTTP_MAX_BODY_SIZE),
case rabbit_mgmt_util:read_complete_body_with_limit(ReqData0, BodySizeLimit) of
{error, Reason} ->
BodySizeLimit = application:get_env(rabbitmq_management, max_http_body_size, ?MANAGEMENT_DEFAULT_HTTP_MAX_BODY_SIZE),
_ = rabbit_log:warning("HTTP API: uploaded definition file exceeded the maximum request body limit of ~p bytes. "
"Use the 'management.http.max_body_size' key in rabbitmq.conf to increase the limit if necessary", [BodySizeLimit]),
rabbit_mgmt_util:bad_request(Reason, ReqData0, Context);
Expand Down

0 comments on commit fb300d2

Please sign in to comment.