aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--doc/src/manual/cowboy_http2.asciidoc42
-rw-r--r--src/cowboy_http2.erl124
-rw-r--r--src/cowboy_stream_h.erl11
-rw-r--r--test/h2spec_SUITE.erl4
-rw-r--r--test/rfc7540_SUITE.erl8
-rw-r--r--test/security_SUITE.erl236
6 files changed, 375 insertions, 50 deletions
diff --git a/doc/src/manual/cowboy_http2.asciidoc b/doc/src/manual/cowboy_http2.asciidoc
index e899289..2764705 100644
--- a/doc/src/manual/cowboy_http2.asciidoc
+++ b/doc/src/manual/cowboy_http2.asciidoc
@@ -26,11 +26,14 @@ opts() :: #{
initial_connection_window_size => 65535..16#7fffffff,
initial_stream_window_size => 0..16#7fffffff,
max_concurrent_streams => non_neg_integer() | infinity,
+ max_connection_buffer_size => non_neg_integer(),
max_connection_window_size => 0..16#7fffffff,
max_decode_table_size => non_neg_integer(),
max_encode_table_size => non_neg_integer(),
max_frame_size_received => 16384..16777215,
max_frame_size_sent => 16384..16777215 | infinity,
+ max_received_frame_rate => {pos_integer(), timeout()},
+ max_reset_stream_rate => {pos_integer(), timeout()},
max_stream_buffer_size => non_neg_integer(),
max_stream_window_size => 0..16#7fffffff,
preface_timeout => timeout(),
@@ -38,6 +41,7 @@ opts() :: #{
sendfile => boolean(),
settings_timeout => timeout(),
stream_handlers => [module()],
+ stream_window_data_threshold => 0..16#7fffffff,
stream_window_margin_size => 0..16#7fffffff,
stream_window_update_threshold => 0..16#7fffffff
}
@@ -104,6 +108,12 @@ max_concurrent_streams (infinity)::
Maximum number of concurrent streams allowed on the connection.
+max_connection_buffer_size (16000000)::
+
+Maximum size of all stream buffers for this connection, in bytes.
+This is a soft limit used to apply backpressure to handlers that
+send data faster than the HTTP/2 connection allows.
+
max_connection_window_size (16#7fffffff)::
Maximum connection window size in bytes. This is used as an upper bound
@@ -137,6 +147,22 @@ following the client's advertised maximum.
Note that actual frame sizes may be lower than the limit when
there is not enough space left in the flow control window.
+max_received_frame_rate ({1000, 10000})::
+
+Maximum frame rate allowed per connection. The rate is expressed
+as a tuple `{NumFrames, TimeMs}` indicating how many frames are
+allowed over the given time period. This is similar to a supervisor
+restart intensity/period.
+
+max_reset_stream_rate ({10, 10000})::
+
+Maximum reset stream rate per connection. This can be used to
+protect against misbehaving or malicious peers that do not follow
+the protocol, leading to the server resetting streams, by limiting
+the number of streams that can be reset over a certain time period.
+The rate is expressed as a tuple `{NumResets, TimeMs}`. This is
+similar to a supervisor restart intensity/period.
+
max_stream_buffer_size (8000000)::
Maximum stream buffer size in bytes. This is a soft limit used
@@ -173,6 +199,13 @@ stream_handlers ([cowboy_stream_h])::
Ordered list of stream handlers that will handle all stream events.
+stream_window_data_threshold (16384)::
+
+Window threshold in bytes below which Cowboy will not attempt
+to send data, with one exception. When Cowboy has data to send
+and the window is high enough, Cowboy will always send the data,
+regardless of this option.
+
stream_window_margin_size (65535)::
Extra amount in bytes to be added to the window size when
@@ -193,9 +226,14 @@ too many `WINDOW_UPDATE` frames.
`max_connection_window_size`, `max_stream_window_size`,
`stream_window_margin_size` and
`stream_window_update_threshold` to configure
- behavior on sending WINDOW_UPDATE frames, and
+ behavior on sending WINDOW_UPDATE frames;
+ `max_connection_buffer_size` and
`max_stream_buffer_size` to apply backpressure
- when sending data too fast.
+ when sending data too fast;
+ `max_received_frame_rate` and `max_reset_stream_rate`
+ to protect against various flood scenarios; and
+ `stream_window_data_threshold` to control how small
+ the DATA frames that Cowboy sends can get.
* *2.6*: The `proxy_header` and `sendfile` options were added.
* *2.4*: Add the options `initial_connection_window_size`,
`initial_stream_window_size`, `max_concurrent_streams`,
diff --git a/src/cowboy_http2.erl b/src/cowboy_http2.erl
index f98478a..c73c167 100644
--- a/src/cowboy_http2.erl
+++ b/src/cowboy_http2.erl
@@ -40,11 +40,14 @@
initial_stream_window_size => 0..16#7fffffff,
logger => module(),
max_concurrent_streams => non_neg_integer() | infinity,
+ max_connection_buffer_size => non_neg_integer(),
max_connection_window_size => 0..16#7fffffff,
max_decode_table_size => non_neg_integer(),
max_encode_table_size => non_neg_integer(),
max_frame_size_received => 16384..16777215,
max_frame_size_sent => 16384..16777215 | infinity,
+ max_received_frame_rate => {pos_integer(), timeout()},
+ max_reset_stream_rate => {pos_integer(), timeout()},
max_stream_buffer_size => non_neg_integer(),
max_stream_window_size => 0..16#7fffffff,
metrics_callback => cowboy_metrics_h:metrics_callback(),
@@ -55,6 +58,7 @@
settings_timeout => timeout(),
shutdown_timeout => timeout(),
stream_handlers => [module()],
+ stream_window_data_threshold => 0..16#7fffffff,
stream_window_margin_size => 0..16#7fffffff,
stream_window_update_threshold => 0..16#7fffffff,
tracer_callback => cowboy_tracer_h:tracer_callback(),
@@ -99,6 +103,14 @@
http2_status :: sequence | settings | upgrade | connected | closing,
http2_machine :: cow_http2_machine:http2_machine(),
+ %% HTTP/2 frame rate flood protection.
+ frame_rate_num :: non_neg_integer(),
+ frame_rate_time :: integer(),
+
+ %% HTTP/2 reset stream flood protection.
+ reset_rate_num :: non_neg_integer(),
+ reset_rate_time :: integer(),
+
%% Flow requested for all streams.
flow = 0 :: non_neg_integer(),
@@ -147,16 +159,28 @@ init(Parent, Ref, Socket, Transport, ProxyHeader, Opts) ->
binary() | undefined, binary()) -> ok.
init(Parent, Ref, Socket, Transport, ProxyHeader, Opts, Peer, Sock, Cert, Buffer) ->
{ok, Preface, HTTP2Machine} = cow_http2_machine:init(server, Opts),
- State = set_timeout(#state{parent=Parent, ref=Ref, socket=Socket,
+ State = set_timeout(init_rate_limiting(#state{parent=Parent, ref=Ref, socket=Socket,
transport=Transport, proxy_header=ProxyHeader,
opts=Opts, peer=Peer, sock=Sock, cert=Cert,
- http2_status=sequence, http2_machine=HTTP2Machine}),
+ http2_status=sequence, http2_machine=HTTP2Machine})),
Transport:send(Socket, Preface),
case Buffer of
<<>> -> loop(State, Buffer);
_ -> parse(State, Buffer)
end.
+init_rate_limiting(State=#state{opts=Opts}) ->
+ {FrameRateNum, FrameRatePeriod} = maps:get(max_received_frame_rate, Opts, {1000, 10000}),
+ {ResetRateNum, ResetRatePeriod} = maps:get(max_reset_stream_rate, Opts, {10, 10000}),
+ CurrentTime = erlang:monotonic_time(millisecond),
+ State#state{
+ frame_rate_num=FrameRateNum, frame_rate_time=add_period(CurrentTime, FrameRatePeriod),
+ reset_rate_num=ResetRateNum, reset_rate_time=add_period(CurrentTime, ResetRatePeriod)
+ }.
+
+add_period(_, infinity) -> infinity;
+add_period(Time, Period) -> Time + Period.
+
%% @todo Add an argument for the request body.
-spec init(pid(), ranch:ref(), inet:socket(), module(),
ranch_proxy_header:proxy_info() | undefined, cowboy:opts(),
@@ -179,7 +203,7 @@ init(Parent, Ref, Socket, Transport, ProxyHeader, Opts, Peer, Sock, Cert, Buffer
<<"connection">> => <<"Upgrade">>,
<<"upgrade">> => <<"h2c">>
}, ?MODULE, undefined}), %% @todo undefined or #{}?
- State = set_timeout(State2#state{http2_status=sequence}),
+ State = set_timeout(init_rate_limiting(State2#state{http2_status=sequence})),
Transport:send(Socket, Preface),
case Buffer of
<<>> -> loop(State, Buffer);
@@ -258,9 +282,9 @@ parse(State=#state{http2_status=Status, http2_machine=HTTP2Machine, streams=Stre
MaxFrameSize = cow_http2_machine:get_local_setting(max_frame_size, HTTP2Machine),
case cow_http2:parse(Data, MaxFrameSize) of
{ok, Frame, Rest} ->
- parse(frame(State, Frame), Rest);
+ parse(frame_rate(State, Frame), Rest);
{ignore, Rest} ->
- parse(ignored_frame(State), Rest);
+ parse(frame_rate(State, ignore), Rest);
{stream_error, StreamID, Reason, Human, Rest} ->
parse(reset_stream(State, StreamID, {stream_error, Reason, Human}), Rest);
Error = {connection_error, _, _} ->
@@ -272,6 +296,30 @@ parse(State=#state{http2_status=Status, http2_machine=HTTP2Machine, streams=Stre
loop(State, Data)
end.
+%% Frame rate flood protection.
+
+frame_rate(State0=#state{opts=Opts, frame_rate_num=Num0, frame_rate_time=Time}, Frame) ->
+ {Result, State} = case Num0 - 1 of
+ 0 ->
+ CurrentTime = erlang:monotonic_time(millisecond),
+ if
+ CurrentTime < Time ->
+ {error, State0};
+ true ->
+ %% When the option has a period of infinity we cannot reach this clause.
+ {Num, Period} = maps:get(max_received_frame_rate, Opts, {1000, 10000}),
+ {ok, State0#state{frame_rate_num=Num, frame_rate_time=CurrentTime + Period}}
+ end;
+ Num ->
+ {ok, State0#state{frame_rate_num=Num}}
+ end,
+ case {Result, Frame} of
+ {ok, ignore} -> ignored_frame(State);
+ {ok, _} -> frame(State, Frame);
+ {error, _} -> terminate(State, {connection_error, enhance_your_calm,
+ 'Frame rate larger than configuration allows. Flood? (CVE-2019-9512, CVE-2019-9515, CVE-2019-9518)'})
+ end.
+
%% Frames received.
%% We do nothing when receiving a lingering DATA frame.
@@ -763,27 +811,42 @@ send_data_frame(State=#state{socket=Socket, transport=Transport,
%% We do this by comparing the HTTP2Machine buffer state before/after for
%% the relevant streams.
maybe_send_data_alarm(State=#state{opts=Opts, http2_machine=HTTP2Machine}, HTTP2Machine0, StreamID) ->
- {ok, BufferSizeBefore} = cow_http2_machine:get_stream_local_buffer_size(StreamID, HTTP2Machine0),
+ ConnBufferSizeBefore = cow_http2_machine:get_connection_local_buffer_size(HTTP2Machine0),
+ ConnBufferSizeAfter = cow_http2_machine:get_connection_local_buffer_size(HTTP2Machine),
+ {ok, StreamBufferSizeBefore} = cow_http2_machine:get_stream_local_buffer_size(StreamID, HTTP2Machine0),
%% When the stream ends up closed after it finished sending data,
%% we do not want to trigger an alarm. We act as if the buffer
%% size did not change.
- BufferSizeAfter = case cow_http2_machine:get_stream_local_buffer_size(StreamID, HTTP2Machine) of
+ StreamBufferSizeAfter = case cow_http2_machine:get_stream_local_buffer_size(StreamID, HTTP2Machine) of
{ok, BSA} -> BSA;
- {error, closed} -> BufferSizeBefore
+ {error, closed} -> StreamBufferSizeBefore
end,
- MaxBufferSize = maps:get(max_stream_buffer_size, Opts, 8000000),
- %% I do not want to document these internal_events yet. I am not yet
+ MaxConnBufferSize = maps:get(max_connection_buffer_size, Opts, 16000000),
+ MaxStreamBufferSize = maps:get(max_stream_buffer_size, Opts, 8000000),
+ %% I do not want to document these internal events yet. I am not yet
%% convinced it should be {alarm, Name, on|off} and not {internal_event, E}
- %% or something else entirely.
+ %% or something else entirely. Though alarms are probably right.
if
- BufferSizeBefore >= MaxBufferSize, BufferSizeAfter < MaxBufferSize ->
- info(State, StreamID, {alarm, stream_buffer_full, off});
- BufferSizeBefore < MaxBufferSize, BufferSizeAfter >= MaxBufferSize ->
- info(State, StreamID, {alarm, stream_buffer_full, on});
+ ConnBufferSizeBefore >= MaxConnBufferSize, ConnBufferSizeAfter < MaxConnBufferSize ->
+ connection_alarm(State, connection_buffer_full, off);
+ ConnBufferSizeBefore < MaxConnBufferSize, ConnBufferSizeAfter >= MaxConnBufferSize ->
+ connection_alarm(State, connection_buffer_full, on);
+ StreamBufferSizeBefore >= MaxStreamBufferSize, StreamBufferSizeAfter < MaxStreamBufferSize ->
+ stream_alarm(State, StreamID, stream_buffer_full, off);
+ StreamBufferSizeBefore < MaxStreamBufferSize, StreamBufferSizeAfter >= MaxStreamBufferSize ->
+ stream_alarm(State, StreamID, stream_buffer_full, on);
true ->
State
end.
+connection_alarm(State0=#state{streams=Streams}, Name, Value) ->
+ lists:foldl(fun(StreamID, State) ->
+ stream_alarm(State, StreamID, Name, Value)
+ end, State0, maps:keys(Streams)).
+
+stream_alarm(State, StreamID, Name, Value) ->
+ info(State, StreamID, {alarm, Name, Value}).
+
%% Terminate a stream or the connection.
%% We may have to cancel streams even if we receive multiple
@@ -855,18 +918,41 @@ terminate_all_streams(State, [{StreamID, #stream{state=StreamState}}|Tail], Reas
terminate_all_streams(State, Tail, Reason).
%% @todo Don't send an RST_STREAM if one was already sent.
-reset_stream(State=#state{socket=Socket, transport=Transport,
+reset_stream(State0=#state{socket=Socket, transport=Transport,
http2_machine=HTTP2Machine0}, StreamID, Error) ->
Reason = case Error of
{internal_error, _, _} -> internal_error;
{stream_error, Reason0, _} -> Reason0
end,
Transport:send(Socket, cow_http2:rst_stream(StreamID, Reason)),
- case cow_http2_machine:reset_stream(StreamID, HTTP2Machine0) of
+ State1 = case cow_http2_machine:reset_stream(StreamID, HTTP2Machine0) of
{ok, HTTP2Machine} ->
- terminate_stream(State#state{http2_machine=HTTP2Machine}, StreamID, Error);
+ terminate_stream(State0#state{http2_machine=HTTP2Machine}, StreamID, Error);
{error, not_found} ->
- terminate_stream(State, StreamID, Error)
+ terminate_stream(State0, StreamID, Error)
+ end,
+ case reset_rate(State1) of
+ {ok, State} ->
+ State;
+ error ->
+ terminate(State1, {connection_error, enhance_your_calm,
+ 'Stream reset rate larger than configuration allows. Flood? (CVE-2019-9514)'})
+ end.
+
+reset_rate(State0=#state{opts=Opts, reset_rate_num=Num0, reset_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.
+ {Num, Period} = maps:get(max_reset_stream_rate, Opts, {10, 10000}),
+ {ok, State0#state{reset_rate_num=Num, reset_rate_time=CurrentTime + Period}}
+ end;
+ Num ->
+ {ok, State0#state{reset_rate_num=Num}}
end.
stop_stream(State=#state{http2_machine=HTTP2Machine}, StreamID) ->
diff --git a/src/cowboy_stream_h.erl b/src/cowboy_stream_h.erl
index cc9e271..9397726 100644
--- a/src/cowboy_stream_h.erl
+++ b/src/cowboy_stream_h.erl
@@ -220,8 +220,8 @@ info(StreamID, Response={response, _, _, _}, State) ->
do_info(StreamID, Response, [Response], State#state{expect=undefined});
info(StreamID, Headers={headers, _, _}, State) ->
do_info(StreamID, Headers, [Headers], State#state{expect=undefined});
-%% Sending data involves the data message and the stream_buffer_full alarm.
-%% We stop sending acks when the alarm is on.
+%% Sending data involves the data message, the stream_buffer_full alarm
+%% and the connection_buffer_full alarm. We stop sending acks when an alarm is on.
info(StreamID, Data={data, _, _}, State0=#state{pid=Pid, stream_body_status=Status}) ->
State = case Status of
normal ->
@@ -233,10 +233,13 @@ info(StreamID, Data={data, _, _}, State0=#state{pid=Pid, stream_body_status=Stat
State0
end,
do_info(StreamID, Data, [Data], State);
-info(StreamID, Alarm={alarm, stream_buffer_full, on}, State) ->
+info(StreamID, Alarm={alarm, Name, on}, State)
+ when Name =:= connection_buffer_full; Name =:= stream_buffer_full ->
do_info(StreamID, Alarm, [], State#state{stream_body_status=blocking});
-info(StreamID, Alarm={alarm, stream_buffer_full, off}, State=#state{pid=Pid, stream_body_status=Status}) ->
+info(StreamID, Alarm={alarm, Name, off}, State=#state{pid=Pid, stream_body_status=Status})
+ when Name =:= connection_buffer_full; Name =:= stream_buffer_full ->
_ = case Status of
+ normal -> ok;
blocking -> ok;
blocked -> Pid ! {data_ack, self()}
end,
diff --git a/test/h2spec_SUITE.erl b/test/h2spec_SUITE.erl
index 2ed9df8..13beea6 100644
--- a/test/h2spec_SUITE.erl
+++ b/test/h2spec_SUITE.erl
@@ -35,7 +35,9 @@ init_per_suite(Config) ->
true ->
cowboy_test:init_http(h2spec, #{
env => #{dispatch => init_dispatch()},
- max_concurrent_streams => 100
+ max_concurrent_streams => 100,
+ %% Disable the DATA threshold for this test suite.
+ stream_window_data_threshold => 0
}, Config)
end
end.
diff --git a/test/rfc7540_SUITE.erl b/test/rfc7540_SUITE.erl
index 4f27dfa..ebd9341 100644
--- a/test/rfc7540_SUITE.erl
+++ b/test/rfc7540_SUITE.erl
@@ -34,11 +34,15 @@ groups() ->
init_per_group(Name = clear, Config) ->
cowboy_test:init_http(Name, #{
- env => #{dispatch => cowboy_router:compile(init_routes(Config))}
+ env => #{dispatch => cowboy_router:compile(init_routes(Config))},
+ %% Disable the DATA threshold for this test suite.
+ stream_window_data_threshold => 0
}, Config);
init_per_group(Name = tls, Config) ->
cowboy_test:init_http2(Name, #{
- env => #{dispatch => cowboy_router:compile(init_routes(Config))}
+ env => #{dispatch => cowboy_router:compile(init_routes(Config))},
+ %% Disable the DATA threshold for this test suite.
+ stream_window_data_threshold => 0
}, Config).
end_per_group(Name, _) ->
diff --git a/test/security_SUITE.erl b/test/security_SUITE.erl
index 4d8a68c..684b78a 100644
--- a/test/security_SUITE.erl
+++ b/test/security_SUITE.erl
@@ -30,7 +30,28 @@ all() ->
cowboy_test:common_all().
groups() ->
- cowboy_test:common_groups(ct_helper:all(?MODULE)).
+ Tests = [nc_rand, nc_zero],
+ H1Tests = [slowloris, slowloris_chunks],
+ H2CTests = [
+ http2_data_dribble,
+ http2_empty_frame_flooding_data,
+ http2_empty_frame_flooding_headers_continuation,
+ http2_empty_frame_flooding_push_promise,
+ http2_ping_flood,
+ http2_reset_flood,
+ http2_settings_flood,
+ http2_zero_length_header_leak
+ ],
+ [
+ {http, [parallel], Tests ++ H1Tests},
+ {https, [parallel], Tests ++ H1Tests},
+ {h2, [parallel], Tests},
+ {h2c, [parallel], Tests ++ H2CTests},
+ {http_compress, [parallel], Tests ++ H1Tests},
+ {https_compress, [parallel], Tests ++ H1Tests},
+ {h2_compress, [parallel], Tests},
+ {h2c_compress, [parallel], Tests ++ H2CTests}
+ ].
init_per_suite(Config) ->
ct_helper:create_static_dir(config(priv_dir, Config) ++ "/static"),
@@ -49,11 +70,200 @@ end_per_group(Name, _) ->
init_dispatch(_) ->
cowboy_router:compile([{"localhost", [
- {"/", hello_h, []}
+ {"/", hello_h, []},
+ {"/echo/:key", echo_h, []},
+ {"/resp/:key[/:arg]", resp_h, []}
]}]).
%% Tests.
+http2_data_dribble(Config) ->
+ doc("Request a very large response then update the window 1 byte at a time. (CVE-2019-9511)"),
+ {ok, Socket} = rfc7540_SUITE:do_handshake(Config),
+ %% Send a GET request for a very large response.
+ {HeadersBlock, _} = cow_hpack:encode([
+ {<<":method">>, <<"GET">>},
+ {<<":scheme">>, <<"http">>},
+ {<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
+ {<<":path">>, <<"/resp/stream_body/loop">>}
+ ]),
+ ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)),
+ %% Receive a response with a few DATA frames draining the window.
+ {ok, <<SkipLen:24, 1:8, _:8, 1:32>>} = gen_tcp:recv(Socket, 9, 1000),
+ {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000),
+ {ok, <<16384:24, 0:8, 0:8, 1:32, _:16384/unit:8>>} = gen_tcp:recv(Socket, 9 + 16384, 1000),
+ {ok, <<16384:24, 0:8, 0:8, 1:32, _:16384/unit:8>>} = gen_tcp:recv(Socket, 9 + 16384, 1000),
+ {ok, <<16384:24, 0:8, 0:8, 1:32, _:16384/unit:8>>} = gen_tcp:recv(Socket, 9 + 16384, 1000),
+ {ok, <<16383:24, 0:8, 0:8, 1:32, _:16383/unit:8>>} = gen_tcp:recv(Socket, 9 + 16383, 1000),
+ %% Send WINDOW_UPDATE frames with a value of 1. The server should
+ %% not attempt to send data until the window is over a configurable threshold.
+ ok = gen_tcp:send(Socket, [
+ cow_http2:window_update(1),
+ cow_http2:window_update(1, 1)
+ ]),
+ {error, timeout} = gen_tcp:recv(Socket, 0, 1000),
+ ok.
+
+http2_empty_frame_flooding_data(Config) ->
+ doc("Confirm that Cowboy detects empty DATA frame flooding. (CVE-2019-9518)"),
+ {ok, Socket} = rfc7540_SUITE:do_handshake(Config),
+ %% Send a POST request followed by many empty DATA frames.
+ {HeadersBlock, _} = cow_hpack:encode([
+ {<<":method">>, <<"POST">>},
+ {<<":scheme">>, <<"http">>},
+ {<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
+ {<<":path">>, <<"/echo/read_body">>}
+ ]),
+ ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, HeadersBlock)),
+ _ = [gen_tcp:send(Socket, cow_http2:data(1, nofin, <<>>)) || _ <- lists:seq(1, 2000)],
+ %% When Cowboy detects a flood it must close the connection.
+ %% We skip WINDOW_UPDATE frames sent when Cowboy starts to read the body.
+ case gen_tcp:recv(Socket, 43, 6000) of
+ {ok, <<_:26/unit:8, _:24, 7:8, _:72, 11:32>>} ->
+ ok;
+ %% We also accept the connection being closed immediately,
+ %% which may happen because we send the GOAWAY right before closing.
+ {error, closed} ->
+ ok
+ end.
+
+http2_empty_frame_flooding_headers_continuation(Config) ->
+ doc("Confirm that Cowboy detects empty HEADERS/CONTINUATION frame flooding. (CVE-2019-9518)"),
+ {ok, Socket} = rfc7540_SUITE:do_handshake(Config),
+ %% Send many empty HEADERS/CONTINUATION frames before the headers.
+ ok = gen_tcp:send(Socket, <<0:24, 1:8, 0:9, 1:31>>),
+ _ = [gen_tcp:send(Socket, <<0:24, 9:8, 0:9, 1:31>>) || _ <- lists:seq(1, 2000)],
+ {HeadersBlock, _} = cow_hpack:encode([
+ {<<":method">>, <<"POST">>},
+ {<<":scheme">>, <<"http">>},
+ {<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
+ {<<":path">>, <<"/">>}
+ ]),
+ Len = iolist_size(HeadersBlock),
+ _ = gen_tcp:send(Socket, [<<Len:24, 9:8, 0:5, 1:1, 0:1, 1:1, 0:1, 1:31>>, HeadersBlock]),
+ %% When Cowboy detects a flood it must close the connection.
+ case gen_tcp:recv(Socket, 17, 6000) of
+ {ok, <<_:24, 7:8, _:72, 11:32>>} ->
+ ok;
+ %% We also accept the connection being closed immediately,
+ %% which may happen because we send the GOAWAY right before closing.
+ {error, closed} ->
+ ok
+ end.
+
+http2_empty_frame_flooding_push_promise(Config) ->
+ doc("Confirm that Cowboy detects empty PUSH_PROMISE frame flooding. (CVE-2019-9518)"),
+ {ok, Socket} = rfc7540_SUITE:do_handshake(Config),
+ %% Send a HEADERS frame to which we will attach a PUSH_PROMISE.
+ %% We use nofin in order to keep the stream alive.
+ {HeadersBlock, _} = cow_hpack:encode([
+ {<<":method">>, <<"GET">>},
+ {<<":scheme">>, <<"http">>},
+ {<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
+ {<<":path">>, <<"/">>}
+ ]),
+ ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, HeadersBlock)),
+ %% Send nofin PUSH_PROMISE frame without any data.
+ ok = gen_tcp:send(Socket, <<4:24, 5:8, 0:8, 0:1, 1:31, 0:1, 3:31>>),
+ %% Receive a PROTOCOL_ERROR connection error.
+ %%
+ %% Cowboy rejects all PUSH_PROMISE frames therefore no flooding
+ %% can take place.
+ {ok, <<_:24, 7:8, _:72, 1:32>>} = gen_tcp:recv(Socket, 17, 6000),
+ ok.
+
+%% @todo http2_internal_data_buffering(Config) -> I do not know how to test this.
+% doc("Request many very large responses, with a larger than necessary window size, "
+% "but do not attempt to read from the socket. (CVE-2019-9517)"),
+
+http2_ping_flood(Config) ->
+ doc("Confirm that Cowboy detects PING floods. (CVE-2019-9512)"),
+ {ok, Socket} = rfc7540_SUITE:do_handshake(Config),
+ %% Flood the server with PING frames.
+ _ = [gen_tcp:send(Socket, cow_http2:ping(0)) || _ <- lists:seq(1, 2000)],
+ %% Receive a number of PING ACK frames in return, following by the closing of the connection.
+ try
+ [case gen_tcp:recv(Socket, 17, 6000) of
+ {ok, <<8:24, 6:8, _:7, 1:1, _:32, 0:64>>} -> ok;
+ {ok, <<_:24, 7:8, _:72, 11:32>>} -> throw(goaway);
+ %% We also accept the connection being closed immediately,
+ %% which may happen because we send the GOAWAY right before closing.
+ {error, closed} -> throw(goaway)
+ end || _ <- lists:seq(1, 2000)],
+ error(flood_successful)
+ catch throw:goaway ->
+ ok
+ end.
+
+http2_reset_flood(Config) ->
+ doc("Confirm that Cowboy detects reset floods. (CVE-2019-9514)"),
+ {ok, Socket} = rfc7540_SUITE:do_handshake(Config),
+ %% Flood the server with HEADERS frames without a :method pseudo-header.
+ {HeadersBlock, _} = cow_hpack:encode([
+ {<<":scheme">>, <<"http">>},
+ {<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
+ {<<":path">>, <<"/">>}
+ ]),
+ _ = [gen_tcp:send(Socket, cow_http2:headers(ID, fin, HeadersBlock)) || ID <- lists:seq(1, 100, 2)],
+ %% Receive a number of RST_STREAM frames in return, following by the closing of the connection.
+ try
+ [case gen_tcp:recv(Socket, 13, 6000) of
+ {ok, <<_:24, 3:8, _:8, ID:32, 1:32>>} -> ok;
+ {ok, <<_:24, 7:8, _:72>>} ->
+ {ok, <<11:32>>} = gen_tcp:recv(Socket, 4, 1000),
+ throw(goaway);
+ %% We also accept the connection being closed immediately,
+ %% which may happen because we send the GOAWAY right before closing.
+ {error, closed} ->
+ throw(goaway)
+ end || ID <- lists:seq(1, 100, 2)],
+ error(flood_successful)
+ catch throw:goaway ->
+ ok
+ end.
+
+%% @todo If we ever implement the PRIORITY mechanism, this test should
+%% be implemented as well. CVE-2019-9513 https://www.kb.cert.org/vuls/id/605641/
+%% http2_resource_loop
+
+http2_settings_flood(Config) ->
+ doc("Confirm that Cowboy detects SETTINGS floods. (CVE-2019-9515)"),
+ {ok, Socket} = rfc7540_SUITE:do_handshake(Config),
+ %% Flood the server with empty SETTINGS frames.
+ _ = [gen_tcp:send(Socket, cow_http2:settings(#{})) || _ <- lists:seq(1, 2000)],
+ %% Receive a number of SETTINGS ACK frames in return, following by the closing of the connection.
+ try
+ [case gen_tcp:recv(Socket, 9, 6000) of
+ {ok, <<0:24, 4:8, 0:7, 1:1, 0:32>>} -> ok;
+ {ok, <<_:24, 7:8, _:40>>} ->
+ {ok, <<_:32, 11:32>>} = gen_tcp:recv(Socket, 8, 1000),
+ throw(goaway);
+ %% We also accept the connection being closed immediately,
+ %% which may happen because we send the GOAWAY right before closing.
+ {error, closed} ->
+ throw(goaway)
+ end || _ <- lists:seq(1, 2000)],
+ error(flood_successful)
+ catch throw:goaway ->
+ ok
+ end.
+
+http2_zero_length_header_leak(Config) ->
+ doc("Confirm that Cowboy rejects HEADERS frame with a 0-length header name. (CVE-2019-9516)"),
+ {ok, Socket} = rfc7540_SUITE:do_handshake(Config),
+ %% Send a GET request with a 0-length header name.
+ {HeadersBlock, _} = cow_hpack:encode([
+ {<<":method">>, <<"GET">>},
+ {<<":scheme">>, <<"http">>},
+ {<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
+ {<<":path">>, <<"/">>},
+ {<<>>, <<"CVE-2019-9516">>}
+ ]),
+ ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)),
+ %% Receive a PROTOCOL_ERROR stream error.
+ {ok, <<_:24, 3:8, _:8, 1:32, 1:32>>} = gen_tcp:recv(Socket, 13, 6000),
+ ok.
+
nc_rand(Config) ->
doc("Throw random garbage at the server, then check if it's still up."),
do_nc(Config, "/dev/urandom").
@@ -67,9 +277,9 @@ do_nc(Config, Input) ->
Nc = os:find_executable("nc"),
case {Cat, Nc} of
{false, _} ->
- {skip, {not_found, cat}};
+ {skip, "The cat executable was not found."};
{_, false} ->
- {skip, {not_found, nc}};
+ {skip, "The nc executable was not found."};
_ ->
StrPort = integer_to_list(config(port, Config)),
_ = [
@@ -84,15 +294,6 @@ do_nc(Config, Input) ->
slowloris(Config) ->
doc("Send request headers one byte at a time. "
"Confirm that the connection gets closed."),
- _ = case config(protocol, Config) of
- http ->
- do_http_slowloris(Config);
- http2 ->
- %% @todo Write an equivalent test for HTTP2.
- ok
- end.
-
-do_http_slowloris(Config) ->
Client = raw_open(Config),
try
[begin
@@ -107,15 +308,6 @@ do_http_slowloris(Config) ->
end.
slowloris_chunks(Config) ->
- _ = case config(protocol, Config) of
- http ->
- do_http_slowloris_chunks(Config);
- http2 ->
- %% @todo Write an equivalent test for HTTP2.
- ok
- end.
-
-do_http_slowloris_chunks(Config) ->
doc("Send request headers one line at a time. "
"Confirm that the connection gets closed."),
Client = raw_open(Config),