0
Fork 0
mirror of https://github.com/ninenines/cowboy.git synced 2025-07-14 12:20:24 +00:00

Add 'max_cancel_stream_rate' config for the rapid reset attack

This commit is contained in:
Viktor Söderqvist 2023-10-31 11:51:02 +01:00
parent 879a6b8bc5
commit 1f5c6a04e2
2 changed files with 51 additions and 7 deletions

View file

@ -39,6 +39,7 @@ opts() :: #{
max_frame_size_sent => 16384..16777215 | infinity, max_frame_size_sent => 16384..16777215 | infinity,
max_received_frame_rate => {pos_integer(), timeout()}, max_received_frame_rate => {pos_integer(), timeout()},
max_reset_stream_rate => {pos_integer(), timeout()}, max_reset_stream_rate => {pos_integer(), timeout()},
max_cancel_stream_rate => {pos_integer(), timeout()},
max_stream_buffer_size => non_neg_integer(), max_stream_buffer_size => non_neg_integer(),
max_stream_window_size => 0..16#7fffffff, max_stream_window_size => 0..16#7fffffff,
preface_timeout => timeout(), preface_timeout => timeout(),
@ -198,6 +199,14 @@ the number of streams that can be reset over a certain time period.
The rate is expressed as a tuple `{NumResets, TimeMs}`. This is The rate is expressed as a tuple `{NumResets, TimeMs}`. This is
similar to a supervisor restart intensity/period. similar to a supervisor restart intensity/period.
max_cancel_stream_rate ({500, 10000})::
Maximum cancel stream rate per connection. This can be used to protect
against the rapid reset attack (CVE-2023-44487), by limiting the
number of streams that the peer can reset over a certain time period.
The rate is expressed as a tuple `{NumCancels, TimeMs}`. This is
similar to a supervisor restart intensity/period.
max_stream_buffer_size (8000000):: max_stream_buffer_size (8000000)::
Maximum stream buffer size in bytes. This is a soft limit used Maximum stream buffer size in bytes. This is a soft limit used

View file

@ -48,6 +48,7 @@
max_frame_size_sent => 16384..16777215 | infinity, max_frame_size_sent => 16384..16777215 | infinity,
max_received_frame_rate => {pos_integer(), timeout()}, max_received_frame_rate => {pos_integer(), timeout()},
max_reset_stream_rate => {pos_integer(), timeout()}, max_reset_stream_rate => {pos_integer(), timeout()},
max_cancel_stream_rate => {pos_integer(), timeout()},
max_stream_buffer_size => non_neg_integer(), max_stream_buffer_size => non_neg_integer(),
max_stream_window_size => 0..16#7fffffff, max_stream_window_size => 0..16#7fffffff,
metrics_callback => cowboy_metrics_h:metrics_callback(), metrics_callback => cowboy_metrics_h:metrics_callback(),
@ -114,6 +115,10 @@
reset_rate_num :: undefined | pos_integer(), reset_rate_num :: undefined | pos_integer(),
reset_rate_time :: undefined | integer(), reset_rate_time :: undefined | integer(),
%% HTTP/2 rapid reset attack protection.
cancel_rate_num :: undefined | pos_integer(),
cancel_rate_time :: undefined | integer(),
%% Flow requested for all streams. %% Flow requested for all streams.
flow = 0 :: non_neg_integer(), flow = 0 :: non_neg_integer(),
@ -173,9 +178,11 @@ init(Parent, Ref, Socket, Transport, ProxyHeader, Opts, Peer, Sock, Cert, Buffer
_ -> parse(State, Buffer) _ -> parse(State, Buffer)
end. end.
init_rate_limiting(State) -> init_rate_limiting(State0) ->
CurrentTime = erlang:monotonic_time(millisecond), CurrentTime = erlang:monotonic_time(millisecond),
init_reset_rate_limiting(init_frame_rate_limiting(State, CurrentTime), CurrentTime). State1 = init_frame_rate_limiting(State0, CurrentTime),
State2 = init_reset_rate_limiting(State1, CurrentTime),
init_cancel_rate_limiting(State2, CurrentTime).
init_frame_rate_limiting(State=#state{opts=Opts}, CurrentTime) -> init_frame_rate_limiting(State=#state{opts=Opts}, CurrentTime) ->
{FrameRateNum, FrameRatePeriod} = maps:get(max_received_frame_rate, Opts, {10000, 10000}), {FrameRateNum, FrameRatePeriod} = maps:get(max_received_frame_rate, Opts, {10000, 10000}),
@ -189,6 +196,12 @@ init_reset_rate_limiting(State=#state{opts=Opts}, CurrentTime) ->
reset_rate_num=ResetRateNum, reset_rate_time=add_period(CurrentTime, ResetRatePeriod) reset_rate_num=ResetRateNum, reset_rate_time=add_period(CurrentTime, ResetRatePeriod)
}. }.
init_cancel_rate_limiting(State=#state{opts=Opts}, CurrentTime) ->
{CancelRateNum, CancelRatePeriod} = maps:get(max_cancel_stream_rate, Opts, {100, 10000}),
State#state{
cancel_rate_num=CancelRateNum, cancel_rate_time=add_period(CurrentTime, CancelRatePeriod)
}.
add_period(_, infinity) -> infinity; add_period(_, infinity) -> infinity;
add_period(Time, Period) -> Time + Period. add_period(Time, Period) -> Time + Period.
@ -563,14 +576,21 @@ early_error(State0=#state{ref=Ref, opts=Opts, peer=Peer},
send_headers(State0, StreamID, fin, StatusCode0, RespHeaders0) send_headers(State0, StreamID, fin, StatusCode0, RespHeaders0)
end. end.
rst_stream_frame(State=#state{streams=Streams0, children=Children0}, StreamID, Reason) -> rst_stream_frame(State0=#state{streams=Streams0, children=Children0}, StreamID, Reason) ->
case maps:take(StreamID, Streams0) of State1 = case maps:take(StreamID, Streams0) of
{#stream{state=StreamState}, Streams} -> {#stream{state=StreamState}, Streams} ->
terminate_stream_handler(State, StreamID, Reason, StreamState), terminate_stream_handler(State0, StreamID, Reason, StreamState),
Children = cowboy_children:shutdown(Children0, StreamID), Children = cowboy_children:shutdown(Children0, StreamID),
State#state{streams=Streams, children=Children}; State0#state{streams=Streams, children=Children};
error -> error ->
State State0
end,
case cancel_rate(State1) of
{ok, State} ->
State;
error ->
terminate(State1, {connection_error, enhance_your_calm,
'Stream cancel rate larger than configuration allows. Flood? (CVE-2023-44487)'})
end. end.
ignored_frame(State=#state{http2_machine=HTTP2Machine0}) -> ignored_frame(State=#state{http2_machine=HTTP2Machine0}) ->
@ -1137,6 +1157,21 @@ reset_rate(State0=#state{reset_rate_num=Num0, reset_rate_time=Time}) ->
{ok, State0#state{reset_rate_num=Num}} {ok, State0#state{reset_rate_num=Num}}
end. end.
cancel_rate(State0=#state{cancel_rate_num=Num0, cancel_rate_time=Time}) ->
case Num0 - 1 of
0 ->
CurrentTime = erlang:monotonic_time(millisecond),
if
CurrentTime < Time ->
error;
true ->
%% When the option has a period of infinity we cannot reach this clause.
{ok, init_cancel_rate_limiting(State0, CurrentTime)}
end;
Num ->
{ok, State0#state{cancel_rate_num=Num}}
end.
stop_stream(State=#state{http2_machine=HTTP2Machine}, StreamID) -> stop_stream(State=#state{http2_machine=HTTP2Machine}, StreamID) ->
case cow_http2_machine:get_stream_local_state(StreamID, HTTP2Machine) of case cow_http2_machine:get_stream_local_state(StreamID, HTTP2Machine) of
%% When the stream terminates normally (without sending RST_STREAM) %% When the stream terminates normally (without sending RST_STREAM)