Browse Source

Add protection against slowloris vulnerability

This changes the behavior of the `timeout` protocol option to
mean "Time in which the full request line and headers must be
received". The default of 5s should be fine for all normal uses.

This change has no noticeable impact on performance and is thus
enabled by default for everyone. It can be disabled by setting
`timeout` to `infinity` although that is definitely not encouraged.

Inspired by the contribution from @naryl on github.
Loïc Hoguin 12 years ago
parent
commit
a013becc66
2 changed files with 72 additions and 16 deletions
  1. 40 16
      src/cowboy_protocol.erl
  2. 32 0
      test/http_SUITE.erl

+ 40 - 16
src/cowboy_protocol.erl

@@ -38,8 +38,8 @@
 %%   not available at this point.</dd>
 %%   not available at this point.</dd>
 %%  <dt>onresponse</dt><dd>Optional fun that allows replacing a response
 %%  <dt>onresponse</dt><dd>Optional fun that allows replacing a response
 %%   sent by the application.</dd>
 %%   sent by the application.</dd>
-%%  <dt>timeout</dt><dd>Time in milliseconds before an idle
-%%   connection is closed. Defaults to 5000 milliseconds.</dd>
+%%  <dt>timeout</dt><dd>Time in milliseconds a client has to send the
+%%   full request line and headers. Defaults to 5000 milliseconds.</dd>
 %% </dl>
 %% </dl>
 %%
 %%
 %% Note that there is no need to monitor these processes when using Cowboy as
 %% Note that there is no need to monitor these processes when using Cowboy as
@@ -74,7 +74,8 @@
 	max_header_name_length :: non_neg_integer(),
 	max_header_name_length :: non_neg_integer(),
 	max_header_value_length :: non_neg_integer(),
 	max_header_value_length :: non_neg_integer(),
 	max_headers :: non_neg_integer(),
 	max_headers :: non_neg_integer(),
-	timeout :: timeout()
+	timeout :: timeout(),
+	until :: non_neg_integer() | infinity
 }).
 }).
 
 
 %% API.
 %% API.
@@ -116,7 +117,15 @@ init(ListenerPid, Socket, Transport, Opts) ->
 		max_request_line_length=MaxRequestLineLength,
 		max_request_line_length=MaxRequestLineLength,
 		max_header_name_length=MaxHeaderNameLength,
 		max_header_name_length=MaxHeaderNameLength,
 		max_header_value_length=MaxHeaderValueLength, max_headers=MaxHeaders,
 		max_header_value_length=MaxHeaderValueLength, max_headers=MaxHeaders,
-		timeout=Timeout, onrequest=OnRequest, onresponse=OnResponse}, 0).
+		onrequest=OnRequest, onresponse=OnResponse,
+		timeout=Timeout, until=until(Timeout)}, 0).
+
+-spec until(timeout()) -> non_neg_integer() | infinity.
+until(infinity) ->
+	infinity;
+until(Timeout) ->
+	{Me, S, Mi} = os:timestamp(),
+	Me * 1000000000 + S * 1000 + Mi div 1000 + Timeout.
 
 
 %% Request parsing.
 %% Request parsing.
 %%
 %%
@@ -125,10 +134,24 @@ init(ListenerPid, Socket, Transport, Opts) ->
 %% right after the header parsing is finished and the code becomes
 %% right after the header parsing is finished and the code becomes
 %% more interesting past that point.
 %% more interesting past that point.
 
 
+-spec recv(inet:socket(), module(), non_neg_integer() | infinity)
+	-> {ok, binary()} | {error, closed | timeout | atom()}.
+recv(Socket, Transport, infinity) ->
+	Transport:recv(Socket, 0, infinity);
+recv(Socket, Transport, Until) ->
+	{Me, S, Mi} = os:timestamp(),
+	Now = Me * 1000000000 + S * 1000 + Mi div 1000,
+	Timeout = Until - Now,
+	if	Timeout < 0 ->
+			{error, timeout};
+		true ->
+			Transport:recv(Socket, 0, Timeout)
+	end.
+
 -spec wait_request(binary(), #state{}, non_neg_integer()) -> ok.
 -spec wait_request(binary(), #state{}, non_neg_integer()) -> ok.
 wait_request(Buffer, State=#state{socket=Socket, transport=Transport,
 wait_request(Buffer, State=#state{socket=Socket, transport=Transport,
-		timeout=Timeout}, ReqEmpty) ->
-	case Transport:recv(Socket, 0, Timeout) of
+		until=Until}, ReqEmpty) ->
+	case recv(Socket, Transport, Until) of
 		{ok, Data} ->
 		{ok, Data} ->
 			parse_request(<< Buffer/binary, Data/binary >>, State, ReqEmpty);
 			parse_request(<< Buffer/binary, Data/binary >>, State, ReqEmpty);
 		{error, _} ->
 		{error, _} ->
@@ -219,8 +242,8 @@ wait_header(_, State=#state{max_headers=MaxHeaders}, _, _, _, _, _, Headers)
 		when length(Headers) >= MaxHeaders ->
 		when length(Headers) >= MaxHeaders ->
 	error_terminate(400, State);
 	error_terminate(400, State);
 wait_header(Buffer, State=#state{socket=Socket, transport=Transport,
 wait_header(Buffer, State=#state{socket=Socket, transport=Transport,
-		timeout=Timeout}, M, P, Q, F, V, H) ->
-	case Transport:recv(Socket, 0, Timeout) of
+		until=Until}, M, P, Q, F, V, H) ->
+	case recv(Socket, Transport, Until) of
 		{ok, Data} ->
 		{ok, Data} ->
 			parse_header(<< Buffer/binary, Data/binary >>,
 			parse_header(<< Buffer/binary, Data/binary >>,
 				State, M, P, Q, F, V, H);
 				State, M, P, Q, F, V, H);
@@ -291,9 +314,9 @@ parse_hd_name_ws(<< C, Rest/bits >>, S, M, P, Q, F, V, H, Name) ->
 	end.
 	end.
 
 
 wait_hd_before_value(Buffer, State=#state{
 wait_hd_before_value(Buffer, State=#state{
-		socket=Socket, transport=Transport, timeout=Timeout},
+		socket=Socket, transport=Transport, until=Until},
 		M, P, Q, F, V, H, N) ->
 		M, P, Q, F, V, H, N) ->
-	case Transport:recv(Socket, 0, Timeout) of
+	case recv(Socket, Transport, Until) of
 		{ok, Data} ->
 		{ok, Data} ->
 			parse_hd_before_value(<< Buffer/binary, Data/binary >>,
 			parse_hd_before_value(<< Buffer/binary, Data/binary >>,
 				State, M, P, Q, F, V, H, N);
 				State, M, P, Q, F, V, H, N);
@@ -323,9 +346,9 @@ parse_hd_before_value(Buffer, State=#state{
 %% to change the other arguments' position and trigger costy
 %% to change the other arguments' position and trigger costy
 %% operations for no reasons.
 %% operations for no reasons.
 wait_hd_value(_, State=#state{
 wait_hd_value(_, State=#state{
-		socket=Socket, transport=Transport, timeout=Timeout},
+		socket=Socket, transport=Transport, until=Until},
 		M, P, Q, F, V, H, N, SoFar) ->
 		M, P, Q, F, V, H, N, SoFar) ->
-	case Transport:recv(Socket, 0, Timeout) of
+	case recv(Socket, Transport, Until) of
 		{ok, Data} ->
 		{ok, Data} ->
 			parse_hd_value(Data, State, M, P, Q, F, V, H, N, SoFar);
 			parse_hd_value(Data, State, M, P, Q, F, V, H, N, SoFar);
 		{error, timeout} ->
 		{error, timeout} ->
@@ -338,9 +361,9 @@ wait_hd_value(_, State=#state{
 %% to check for multilines allows us to avoid a few tests in
 %% to check for multilines allows us to avoid a few tests in
 %% the critical path, but forces us to have a special function.
 %% the critical path, but forces us to have a special function.
 wait_hd_value_nl(_, State=#state{
 wait_hd_value_nl(_, State=#state{
-		socket=Socket, transport=Transport, timeout=Timeout},
+		socket=Socket, transport=Transport, until=Until},
 		M, P, Q, F, V, Headers, Name, SoFar) ->
 		M, P, Q, F, V, Headers, Name, SoFar) ->
-	case Transport:recv(Socket, 0, Timeout) of
+	case recv(Socket, Transport, Until) of
 		{ok, << C, Data/bits >>} when C =:= $\s; C =:= $\t  ->
 		{ok, << C, Data/bits >>} when C =:= $\s; C =:= $\t  ->
 			parse_hd_value(Data, State, M, P, Q, F, V, Headers, Name, SoFar);
 			parse_hd_value(Data, State, M, P, Q, F, V, Headers, Name, SoFar);
 		{ok, Data} ->
 		{ok, Data} ->
@@ -492,7 +515,8 @@ resume(State, Env, Tail, Module, Function, Args) ->
 	end.
 	end.
 
 
 -spec next_request(cowboy_req:req(), #state{}, any()) -> ok.
 -spec next_request(cowboy_req:req(), #state{}, any()) -> ok.
-next_request(Req, State=#state{req_keepalive=Keepalive}, HandlerRes) ->
+next_request(Req, State=#state{req_keepalive=Keepalive, timeout=Timeout},
+		HandlerRes) ->
 	cowboy_req:ensure_response(Req, 204),
 	cowboy_req:ensure_response(Req, 204),
 	{BodyRes, [Buffer, Connection]} = case cowboy_req:skip_body(Req) of
 	{BodyRes, [Buffer, Connection]} = case cowboy_req:skip_body(Req) of
 		{ok, Req2} -> {ok, cowboy_req:get([buffer, connection], Req2)};
 		{ok, Req2} -> {ok, cowboy_req:get([buffer, connection], Req2)};
@@ -503,7 +527,7 @@ next_request(Req, State=#state{req_keepalive=Keepalive}, HandlerRes) ->
 	case {HandlerRes, BodyRes, Connection} of
 	case {HandlerRes, BodyRes, Connection} of
 		{ok, ok, keepalive} ->
 		{ok, ok, keepalive} ->
 			?MODULE:parse_request(Buffer, State#state{
 			?MODULE:parse_request(Buffer, State#state{
-				req_keepalive=Keepalive + 1}, 0);
+				req_keepalive=Keepalive + 1, until=until(Timeout)}, 0);
 		_Closed ->
 		_Closed ->
 			terminate(State)
 			terminate(State)
 	end.
 	end.

+ 32 - 0
test/http_SUITE.erl

@@ -60,6 +60,8 @@
 -export([set_resp_body/1]).
 -export([set_resp_body/1]).
 -export([set_resp_header/1]).
 -export([set_resp_header/1]).
 -export([set_resp_overwrite/1]).
 -export([set_resp_overwrite/1]).
+-export([slowloris/1]).
+-export([slowloris2/1]).
 -export([static_attribute_etag/1]).
 -export([static_attribute_etag/1]).
 -export([static_function_etag/1]).
 -export([static_function_etag/1]).
 -export([static_mimetypes_function/1]).
 -export([static_mimetypes_function/1]).
@@ -110,6 +112,8 @@ groups() ->
 		set_resp_body,
 		set_resp_body,
 		set_resp_header,
 		set_resp_header,
 		set_resp_overwrite,
 		set_resp_overwrite,
+		slowloris,
+		slowloris2,
 		static_attribute_etag,
 		static_attribute_etag,
 		static_function_etag,
 		static_function_etag,
 		static_mimetypes_function,
 		static_mimetypes_function,
@@ -812,6 +816,34 @@ set_resp_overwrite(Config) ->
 	{<<"server">>, <<"DesireDrive/1.0">>}
 	{<<"server">>, <<"DesireDrive/1.0">>}
 		= lists:keyfind(<<"server">>, 1, Headers).
 		= lists:keyfind(<<"server">>, 1, Headers).
 
 
+slowloris(Config) ->
+	Client = ?config(client, Config),
+	Transport = ?config(transport, Config),
+	{ok, Client2} = cowboy_client:connect(
+		Transport, "localhost", ?config(port, Config), Client),
+	try
+		[begin
+			{ok, _} = cowboy_client:raw_request([C], Client2),
+			receive after 25 -> ok end
+		end || C <- "GET / HTTP/1.1\r\nHost: localhost\r\n"
+			"User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US)\r\n"
+			"Cookie: name=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n\r\n"],
+		error(failure)
+	catch error:{badmatch, _} ->
+		ok
+	end.
+
+slowloris2(Config) ->
+	Client = ?config(client, Config),
+	Transport = ?config(transport, Config),
+	{ok, Client2} = cowboy_client:connect(
+		Transport, "localhost", ?config(port, Config), Client),
+	{ok, _} = cowboy_client:raw_request("GET / HTTP/1.1\r\n", Client2),
+	receive after 300 -> ok end,
+	{ok, _} = cowboy_client:raw_request("Host: localhost\r\n", Client2),
+	receive after 300 -> ok end,
+	{ok, 408, _, _} = cowboy_client:response(Client2).
+
 static_attribute_etag(Config) ->
 static_attribute_etag(Config) ->
 	Client = ?config(client, Config),
 	Client = ?config(client, Config),
 	{ok, Client2} = cowboy_client:request(<<"GET">>,
 	{ok, Client2} = cowboy_client:request(<<"GET">>,