mirror of
https://github.com/ninenines/cowboy.git
synced 2025-07-14 12:20:24 +00:00
Add more validation of absolute-form request targets
This commit is contained in:
parent
5cb244eb7a
commit
2f9ab91cdd
2 changed files with 79 additions and 46 deletions
|
@ -26,6 +26,7 @@
|
||||||
idle_timeout => timeout(),
|
idle_timeout => timeout(),
|
||||||
inactivity_timeout => timeout(),
|
inactivity_timeout => timeout(),
|
||||||
linger_timeout => timeout(),
|
linger_timeout => timeout(),
|
||||||
|
max_authority_length => non_neg_integer(),
|
||||||
max_empty_lines => non_neg_integer(),
|
max_empty_lines => non_neg_integer(),
|
||||||
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(),
|
||||||
|
@ -46,6 +47,7 @@
|
||||||
|
|
||||||
-record(ps_header, {
|
-record(ps_header, {
|
||||||
method = undefined :: binary(),
|
method = undefined :: binary(),
|
||||||
|
authority = undefined :: binary() | undefined,
|
||||||
path = undefined :: binary(),
|
path = undefined :: binary(),
|
||||||
qs = undefined :: binary(),
|
qs = undefined :: binary(),
|
||||||
version = undefined :: cowboy:http_version(),
|
version = undefined :: cowboy:http_version(),
|
||||||
|
@ -347,7 +349,7 @@ parse_request(Buffer, State=#state{opts=Opts, in_streamid=InStreamID}, EmptyLine
|
||||||
case Buffer of
|
case Buffer of
|
||||||
%% @todo * is only for server-wide OPTIONS request (RFC7230 5.3.4); tests
|
%% @todo * is only for server-wide OPTIONS request (RFC7230 5.3.4); tests
|
||||||
<< "OPTIONS * ", Rest/bits >> ->
|
<< "OPTIONS * ", Rest/bits >> ->
|
||||||
parse_version(Rest, State, <<"OPTIONS">>, <<"*">>, <<>>);
|
parse_version(Rest, State, <<"OPTIONS">>, undefined, <<"*">>, <<>>);
|
||||||
<<"CONNECT ", _/bits>> ->
|
<<"CONNECT ", _/bits>> ->
|
||||||
error_terminate(501, State, {connection_error, no_error,
|
error_terminate(501, State, {connection_error, no_error,
|
||||||
'The CONNECT method is currently not implemented. (RFC7231 4.3.6)'});
|
'The CONNECT method is currently not implemented. (RFC7231 4.3.6)'});
|
||||||
|
@ -387,18 +389,28 @@ parse_method(<< C, Rest/bits >>, State, SoFar, Remaining) ->
|
||||||
parse_uri(<< H, T, T, P, "://", Rest/bits >>, State, Method)
|
parse_uri(<< H, T, T, P, "://", Rest/bits >>, State, Method)
|
||||||
when H =:= $h orelse H =:= $H, T =:= $t orelse T =:= $T;
|
when H =:= $h orelse H =:= $H, T =:= $t orelse T =:= $T;
|
||||||
P =:= $p orelse P =:= $P ->
|
P =:= $p orelse P =:= $P ->
|
||||||
parse_uri_skip_host(Rest, State, Method, <<>>);
|
parse_uri_authority(Rest, State, Method);
|
||||||
parse_uri(<< H, T, T, P, S, "://", Rest/bits >>, State, Method)
|
parse_uri(<< H, T, T, P, S, "://", Rest/bits >>, State, Method)
|
||||||
when H =:= $h orelse H =:= $H, T =:= $t orelse T =:= $T;
|
when H =:= $h orelse H =:= $H, T =:= $t orelse T =:= $T;
|
||||||
P =:= $p orelse P =:= $P; S =:= $s orelse S =:= $S ->
|
P =:= $p orelse P =:= $P; S =:= $s orelse S =:= $S ->
|
||||||
parse_uri_skip_host(Rest, State, Method, <<>>);
|
parse_uri_authority(Rest, State, Method);
|
||||||
parse_uri(<< $/, Rest/bits >>, State, Method) ->
|
parse_uri(<< $/, Rest/bits >>, State, Method) ->
|
||||||
parse_uri_path(Rest, State, Method, << $/ >>);
|
parse_uri_path(Rest, State, Method, undefined, <<$/>>);
|
||||||
parse_uri(_, State, _) ->
|
parse_uri(_, State, _) ->
|
||||||
error_terminate(400, State, {connection_error, protocol_error,
|
error_terminate(400, State, {connection_error, protocol_error,
|
||||||
'Invalid request-line or request-target. (RFC7230 3.1.1, RFC7230 5.3)'}).
|
'Invalid request-line or request-target. (RFC7230 3.1.1, RFC7230 5.3)'}).
|
||||||
|
|
||||||
parse_uri_skip_host(<< C, Rest/bits >>, State, Method, SoFar) ->
|
%% @todo We probably want to apply max_authority_length also
|
||||||
|
%% to the host header and to document this option. It might
|
||||||
|
%% also be useful for HTTP/2 requests.
|
||||||
|
parse_uri_authority(Rest, State=#state{opts=Opts}, Method) ->
|
||||||
|
parse_uri_authority(Rest, State, Method, <<>>,
|
||||||
|
maps:get(max_authority_length, Opts, 255)).
|
||||||
|
|
||||||
|
parse_uri_authority(_, State, _, _, 0) ->
|
||||||
|
error_terminate(414, State, {connection_error, limit_reached,
|
||||||
|
'The authority component of the absolute URI is longer than configuration allows. (RFC7230 2.7.1)'});
|
||||||
|
parse_uri_authority(<<C, Rest/bits>>, State, Method, SoFar, Remaining) ->
|
||||||
case C of
|
case C of
|
||||||
$\r ->
|
$\r ->
|
||||||
error_terminate(400, State, {connection_error, protocol_error,
|
error_terminate(400, State, {connection_error, protocol_error,
|
||||||
|
@ -409,58 +421,61 @@ parse_uri_skip_host(<< C, Rest/bits >>, State, Method, SoFar) ->
|
||||||
C when SoFar =:= <<>> andalso
|
C when SoFar =:= <<>> andalso
|
||||||
((C =:= $/) orelse (C =:= $\s) orelse (C =:= $?) orelse (C =:= $#)) ->
|
((C =:= $/) orelse (C =:= $\s) orelse (C =:= $?) orelse (C =:= $#)) ->
|
||||||
error_terminate(400, State, {connection_error, protocol_error,
|
error_terminate(400, State, {connection_error, protocol_error,
|
||||||
'Absolute URIs must include an authority component. (RFC7230 2.7.1)'});
|
'Absolute URIs must include a non-empty host component. (RFC7230 2.7.1)'});
|
||||||
$/ -> parse_uri_path(Rest, State, Method, <<"/">>);
|
$: when SoFar =:= <<>> ->
|
||||||
$\s -> parse_version(Rest, State, Method, <<"/">>, <<>>);
|
error_terminate(400, State, {connection_error, protocol_error,
|
||||||
$? -> parse_uri_query(Rest, State, Method, <<"/">>, <<>>);
|
'Absolute URIs must include a non-empty host component. (RFC7230 2.7.1)'});
|
||||||
$# -> skip_uri_fragment(Rest, State, Method, <<"/">>, <<>>);
|
$/ -> parse_uri_path(Rest, State, Method, SoFar, <<"/">>);
|
||||||
C -> parse_uri_skip_host(Rest, State, Method, <<SoFar/binary, C>>)
|
$\s -> parse_version(Rest, State, Method, SoFar, <<"/">>, <<>>);
|
||||||
|
$? -> parse_uri_query(Rest, State, Method, SoFar, <<"/">>, <<>>);
|
||||||
|
$# -> skip_uri_fragment(Rest, State, Method, SoFar, <<"/">>, <<>>);
|
||||||
|
C -> parse_uri_authority(Rest, State, Method, <<SoFar/binary, C>>, Remaining - 1)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
parse_uri_path(<< C, Rest/bits >>, State, Method, SoFar) ->
|
parse_uri_path(<<C, Rest/bits>>, State, Method, Authority, SoFar) ->
|
||||||
case C of
|
case C of
|
||||||
$\r -> error_terminate(400, State, {connection_error, protocol_error,
|
$\r -> error_terminate(400, State, {connection_error, protocol_error,
|
||||||
'The request-target must not be followed by a line break. (RFC7230 3.1.1)'});
|
'The request-target must not be followed by a line break. (RFC7230 3.1.1)'});
|
||||||
$\s -> parse_version(Rest, State, Method, SoFar, <<>>);
|
$\s -> parse_version(Rest, State, Method, Authority, SoFar, <<>>);
|
||||||
$? -> parse_uri_query(Rest, State, Method, SoFar, <<>>);
|
$? -> parse_uri_query(Rest, State, Method, Authority, SoFar, <<>>);
|
||||||
$# -> skip_uri_fragment(Rest, State, Method, SoFar, <<>>);
|
$# -> skip_uri_fragment(Rest, State, Method, Authority, SoFar, <<>>);
|
||||||
_ -> parse_uri_path(Rest, State, Method, << SoFar/binary, C >>)
|
_ -> parse_uri_path(Rest, State, Method, Authority, <<SoFar/binary, C>>)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
parse_uri_query(<< C, Rest/bits >>, State, M, P, SoFar) ->
|
parse_uri_query(<<C, Rest/bits>>, State, M, A, P, SoFar) ->
|
||||||
case C of
|
case C of
|
||||||
$\r -> error_terminate(400, State, {connection_error, protocol_error,
|
$\r -> error_terminate(400, State, {connection_error, protocol_error,
|
||||||
'The request-target must not be followed by a line break. (RFC7230 3.1.1)'});
|
'The request-target must not be followed by a line break. (RFC7230 3.1.1)'});
|
||||||
$\s -> parse_version(Rest, State, M, P, SoFar);
|
$\s -> parse_version(Rest, State, M, A, P, SoFar);
|
||||||
$# -> skip_uri_fragment(Rest, State, M, P, SoFar);
|
$# -> skip_uri_fragment(Rest, State, M, A, P, SoFar);
|
||||||
_ -> parse_uri_query(Rest, State, M, P, << SoFar/binary, C >>)
|
_ -> parse_uri_query(Rest, State, M, A, P, <<SoFar/binary, C>>)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
skip_uri_fragment(<< C, Rest/bits >>, State, M, P, Q) ->
|
skip_uri_fragment(<<C, Rest/bits>>, State, M, A, P, Q) ->
|
||||||
case C of
|
case C of
|
||||||
$\r -> error_terminate(400, State, {connection_error, protocol_error,
|
$\r -> error_terminate(400, State, {connection_error, protocol_error,
|
||||||
'The request-target must not be followed by a line break. (RFC7230 3.1.1)'});
|
'The request-target must not be followed by a line break. (RFC7230 3.1.1)'});
|
||||||
$\s -> parse_version(Rest, State, M, P, Q);
|
$\s -> parse_version(Rest, State, M, A, P, Q);
|
||||||
_ -> skip_uri_fragment(Rest, State, M, P, Q)
|
_ -> skip_uri_fragment(Rest, State, M, A, P, Q)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
parse_version(<< "HTTP/1.1\r\n", Rest/bits >>, State, M, P, Q) ->
|
parse_version(<< "HTTP/1.1\r\n", Rest/bits >>, State, M, A, P, Q) ->
|
||||||
parse_headers(Rest, State, M, P, Q, 'HTTP/1.1');
|
before_parse_headers(Rest, State, M, A, P, Q, 'HTTP/1.1');
|
||||||
parse_version(<< "HTTP/1.0\r\n", Rest/bits >>, State, M, P, Q) ->
|
parse_version(<< "HTTP/1.0\r\n", Rest/bits >>, State, M, A, P, Q) ->
|
||||||
parse_headers(Rest, State, M, P, Q, 'HTTP/1.0');
|
before_parse_headers(Rest, State, M, A, P, Q, 'HTTP/1.0');
|
||||||
parse_version(<< "HTTP/1.", _, C, _/bits >>, State, _, _, _) when C =:= $\s; C =:= $\t ->
|
parse_version(<< "HTTP/1.", _, C, _/bits >>, State, _, _, _, _) when C =:= $\s; C =:= $\t ->
|
||||||
error_terminate(400, State, {connection_error, protocol_error,
|
error_terminate(400, State, {connection_error, protocol_error,
|
||||||
'Whitespace is not allowed after the HTTP version. (RFC7230 3.1.1)'});
|
'Whitespace is not allowed after the HTTP version. (RFC7230 3.1.1)'});
|
||||||
parse_version(<< C, _/bits >>, State, _, _, _) when C =:= $\s; C =:= $\t ->
|
parse_version(<< C, _/bits >>, State, _, _, _, _) when C =:= $\s; C =:= $\t ->
|
||||||
error_terminate(400, State, {connection_error, protocol_error,
|
error_terminate(400, State, {connection_error, protocol_error,
|
||||||
'The separator between request target and version must be a single SP. (RFC7230 3.1.1)'});
|
'The separator between request target and version must be a single SP. (RFC7230 3.1.1)'});
|
||||||
parse_version(_, State, _, _, _) ->
|
parse_version(_, State, _, _, _, _) ->
|
||||||
error_terminate(505, State, {connection_error, protocol_error,
|
error_terminate(505, State, {connection_error, protocol_error,
|
||||||
'Unsupported HTTP version. (RFC7230 2.6)'}).
|
'Unsupported HTTP version. (RFC7230 2.6)'}).
|
||||||
|
|
||||||
parse_headers(Rest, State, M, P, Q, V) ->
|
before_parse_headers(Rest, State, M, A, P, Q, V) ->
|
||||||
parse_header(Rest, State#state{in_state=#ps_header{
|
parse_header(Rest, State#state{in_state=#ps_header{
|
||||||
method=M, path=P, qs=Q, version=V}}, #{}).
|
method=M, authority=A, path=P, qs=Q, version=V}}, #{}).
|
||||||
|
|
||||||
%% Headers.
|
%% Headers.
|
||||||
|
|
||||||
|
@ -578,7 +593,7 @@ horse_clean_value_ws_end() ->
|
||||||
-endif.
|
-endif.
|
||||||
|
|
||||||
request(Buffer, State=#state{transport=Transport, in_streamid=StreamID,
|
request(Buffer, State=#state{transport=Transport, in_streamid=StreamID,
|
||||||
in_state=PS=#ps_header{version=Version}}, Headers) ->
|
in_state=PS=#ps_header{authority=Authority, version=Version}}, Headers) ->
|
||||||
case maps:get(<<"host">>, Headers, undefined) of
|
case maps:get(<<"host">>, Headers, undefined) of
|
||||||
undefined when Version =:= 'HTTP/1.1' ->
|
undefined when Version =:= 'HTTP/1.1' ->
|
||||||
%% @todo Might want to not close the connection on this and next one.
|
%% @todo Might want to not close the connection on this and next one.
|
||||||
|
@ -587,17 +602,34 @@ request(Buffer, State=#state{transport=Transport, in_streamid=StreamID,
|
||||||
'HTTP/1.1 requests must include a host header. (RFC7230 5.4)'});
|
'HTTP/1.1 requests must include a host header. (RFC7230 5.4)'});
|
||||||
undefined ->
|
undefined ->
|
||||||
request(Buffer, State, Headers, <<>>, default_port(Transport:secure()));
|
request(Buffer, State, Headers, <<>>, default_port(Transport:secure()));
|
||||||
RawHost ->
|
%% @todo When CONNECT requests come in we need to ignore the RawHost
|
||||||
try cow_http_hd:parse_host(RawHost) of
|
%% and instead use the Authority as the source of host.
|
||||||
{Host, undefined} ->
|
RawHost when Authority =:= undefined; Authority =:= RawHost ->
|
||||||
request(Buffer, State, Headers, Host, default_port(Transport:secure()));
|
request_parse_host(Buffer, State, Headers, RawHost);
|
||||||
{Host, Port} ->
|
%% RFC7230 does not explicitly ask us to reject requests
|
||||||
request(Buffer, State, Headers, Host, Port)
|
%% that have a different authority component and host header.
|
||||||
catch _:_ ->
|
%% However it DOES ask clients to set them to the same value,
|
||||||
error_terminate(400, State#state{in_state=PS#ps_header{headers=Headers}},
|
%% so we enforce that.
|
||||||
{stream_error, StreamID, protocol_error,
|
_ ->
|
||||||
'The host header is invalid. (RFC7230 5.4)'})
|
error_terminate(400, State#state{in_state=PS#ps_header{headers=Headers}},
|
||||||
end
|
{stream_error, StreamID, protocol_error,
|
||||||
|
'The host header is different than the absolute-form authority component. (RFC7230 5.4)'})
|
||||||
|
end.
|
||||||
|
|
||||||
|
request_parse_host(Buffer, State=#state{transport=Transport,
|
||||||
|
in_streamid=StreamID, in_state=PS}, Headers, RawHost) ->
|
||||||
|
try cow_http_hd:parse_host(RawHost) of
|
||||||
|
{Host, undefined} ->
|
||||||
|
request(Buffer, State, Headers, Host, default_port(Transport:secure()));
|
||||||
|
{Host, Port} when Port > 0, Port =< 65535 ->
|
||||||
|
request(Buffer, State, Headers, Host, Port);
|
||||||
|
_ ->
|
||||||
|
error_terminate(400, State, {stream_error, StreamID, protocol_error,
|
||||||
|
'The port component of the absolute-form is not in the range 0..65535. (RFC7230 2.7.1)'})
|
||||||
|
catch _:_ ->
|
||||||
|
error_terminate(400, State#state{in_state=PS#ps_header{headers=Headers}},
|
||||||
|
{stream_error, StreamID, protocol_error,
|
||||||
|
'The host header is invalid. (RFC7230 5.4)'})
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec default_port(boolean()) -> 80 | 443.
|
-spec default_port(boolean()) -> 80 | 443.
|
||||||
|
|
|
@ -375,7 +375,7 @@ absolute_form_case_insensitive_host(Config) ->
|
||||||
Echo = <<"http://localhost/echo/uri">>,
|
Echo = <<"http://localhost/echo/uri">>,
|
||||||
#{code := 200, body := Echo} = do_raw(Config,
|
#{code := 200, body := Echo} = do_raw(Config,
|
||||||
"GET http://LoCaLHOsT/echo/uri HTTP/1.1\r\n"
|
"GET http://LoCaLHOsT/echo/uri HTTP/1.1\r\n"
|
||||||
"Host: localhost\r\n"
|
"Host: LoCaLHOsT\r\n"
|
||||||
"\r\n").
|
"\r\n").
|
||||||
|
|
||||||
absolute_form_reject_unknown_schemes(Config) ->
|
absolute_form_reject_unknown_schemes(Config) ->
|
||||||
|
@ -492,7 +492,8 @@ absolute_form_invalid_port_0(Config) ->
|
||||||
{error, closed} = raw_recv(Client, 0, 1000).
|
{error, closed} = raw_recv(Client, 0, 1000).
|
||||||
|
|
||||||
absolute_form_invalid_port_65536(Config) ->
|
absolute_form_invalid_port_65536(Config) ->
|
||||||
doc("Port numbers above 65535 are invalid. The request must be rejected and the connection closed."),
|
doc("Port numbers above 65535 are invalid. The request must be rejected "
|
||||||
|
"and the connection closed."),
|
||||||
#{code := 400, client := Client} = do_raw(Config,
|
#{code := 400, client := Client} = do_raw(Config,
|
||||||
"GET http://localhost:65536/ HTTP/1.1\r\n"
|
"GET http://localhost:65536/ HTTP/1.1\r\n"
|
||||||
"Host: localhost:65536\r\n"
|
"Host: localhost:65536\r\n"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue