Merge pull request #10556 from sstrigler/wrap_auth_headers
fix(emqx_connector_http): wrap and unwrap auth headers
This commit is contained in:
commit
30da70692c
|
@ -473,7 +473,7 @@ preprocess_request(
|
||||||
method => emqx_plugin_libs_rule:preproc_tmpl(to_bin(Method)),
|
method => emqx_plugin_libs_rule:preproc_tmpl(to_bin(Method)),
|
||||||
path => emqx_plugin_libs_rule:preproc_tmpl(Path),
|
path => emqx_plugin_libs_rule:preproc_tmpl(Path),
|
||||||
body => maybe_preproc_tmpl(body, Req),
|
body => maybe_preproc_tmpl(body, Req),
|
||||||
headers => preproc_headers(Headers),
|
headers => wrap_auth_header(preproc_headers(Headers)),
|
||||||
request_timeout => maps:get(request_timeout, Req, 30000),
|
request_timeout => maps:get(request_timeout, Req, 30000),
|
||||||
max_retries => maps:get(max_retries, Req, 2)
|
max_retries => maps:get(max_retries, Req, 2)
|
||||||
}.
|
}.
|
||||||
|
@ -503,6 +503,36 @@ preproc_headers(Headers) when is_list(Headers) ->
|
||||||
Headers
|
Headers
|
||||||
).
|
).
|
||||||
|
|
||||||
|
wrap_auth_header(Headers) ->
|
||||||
|
lists:map(fun maybe_wrap_auth_header/1, Headers).
|
||||||
|
|
||||||
|
maybe_wrap_auth_header({[{str, Key}] = StrKey, Val}) ->
|
||||||
|
{_, MaybeWrapped} = maybe_wrap_auth_header({Key, Val}),
|
||||||
|
{StrKey, MaybeWrapped};
|
||||||
|
maybe_wrap_auth_header({Key, Val} = Header) when
|
||||||
|
is_binary(Key), (size(Key) =:= 19 orelse size(Key) =:= 13)
|
||||||
|
->
|
||||||
|
%% We check the size of potential keys in the guard above and consider only
|
||||||
|
%% those that match the number of characters of either "Authorization" or
|
||||||
|
%% "Proxy-Authorization".
|
||||||
|
case try_bin_to_lower(Key) of
|
||||||
|
<<"authorization">> ->
|
||||||
|
{Key, emqx_secret:wrap(Val)};
|
||||||
|
<<"proxy-authorization">> ->
|
||||||
|
{Key, emqx_secret:wrap(Val)};
|
||||||
|
_Other ->
|
||||||
|
Header
|
||||||
|
end;
|
||||||
|
maybe_wrap_auth_header(Header) ->
|
||||||
|
Header.
|
||||||
|
|
||||||
|
try_bin_to_lower(Bin) ->
|
||||||
|
try iolist_to_binary(string:lowercase(Bin)) of
|
||||||
|
LowercaseBin -> LowercaseBin
|
||||||
|
catch
|
||||||
|
_:_ -> Bin
|
||||||
|
end.
|
||||||
|
|
||||||
maybe_preproc_tmpl(Key, Conf) ->
|
maybe_preproc_tmpl(Key, Conf) ->
|
||||||
case maps:get(Key, Conf, undefined) of
|
case maps:get(Key, Conf, undefined) of
|
||||||
undefined -> undefined;
|
undefined -> undefined;
|
||||||
|
@ -537,7 +567,7 @@ proc_headers(HeaderTks, Msg) ->
|
||||||
fun({K, V}) ->
|
fun({K, V}) ->
|
||||||
{
|
{
|
||||||
emqx_plugin_libs_rule:proc_tmpl(K, Msg),
|
emqx_plugin_libs_rule:proc_tmpl(K, Msg),
|
||||||
emqx_plugin_libs_rule:proc_tmpl(V, Msg)
|
emqx_plugin_libs_rule:proc_tmpl(emqx_secret:unwrap(V), Msg)
|
||||||
}
|
}
|
||||||
end,
|
end,
|
||||||
HeaderTks
|
HeaderTks
|
||||||
|
@ -628,21 +658,13 @@ is_sensitive_key([{str, StringKey}]) ->
|
||||||
is_sensitive_key(Atom) when is_atom(Atom) ->
|
is_sensitive_key(Atom) when is_atom(Atom) ->
|
||||||
is_sensitive_key(erlang:atom_to_binary(Atom));
|
is_sensitive_key(erlang:atom_to_binary(Atom));
|
||||||
is_sensitive_key(Bin) when is_binary(Bin), (size(Bin) =:= 19 orelse size(Bin) =:= 13) ->
|
is_sensitive_key(Bin) when is_binary(Bin), (size(Bin) =:= 19 orelse size(Bin) =:= 13) ->
|
||||||
try
|
%% We want to convert this to lowercase since the http header fields
|
||||||
%% This is wrapped in a try-catch since we don't know that Bin is a
|
%% are case insensitive, which means that a user of the Webhook bridge
|
||||||
%% valid string so string:lowercase/1 might throw an exception.
|
%% can write this field name in many different ways.
|
||||||
%%
|
case try_bin_to_lower(Bin) of
|
||||||
%% We want to convert this to lowercase since the http header fields
|
<<"authorization">> -> true;
|
||||||
%% are case insensitive, which means that a user of the Webhook bridge
|
<<"proxy-authorization">> -> true;
|
||||||
%% can write this field name in many different ways.
|
_ -> false
|
||||||
LowercaseBin = iolist_to_binary(string:lowercase(Bin)),
|
|
||||||
case LowercaseBin of
|
|
||||||
<<"authorization">> -> true;
|
|
||||||
<<"proxy-authorization">> -> true;
|
|
||||||
_ -> false
|
|
||||||
end
|
|
||||||
catch
|
|
||||||
_:_ -> false
|
|
||||||
end;
|
end;
|
||||||
is_sensitive_key(_) ->
|
is_sensitive_key(_) ->
|
||||||
false.
|
false.
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
%% you may not use this file except in compliance with the License.
|
||||||
|
%% You may obtain a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
%% See the License for the specific language governing permissions and
|
||||||
|
%% limitations under the License.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
-module(emqx_connector_http_tests).
|
||||||
|
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
-define(MY_SECRET, <<"my_precious">>).
|
||||||
|
|
||||||
|
wrap_auth_headers_test_() ->
|
||||||
|
{setup,
|
||||||
|
fun() ->
|
||||||
|
meck:expect(ehttpc_sup, start_pool, 2, {ok, foo}),
|
||||||
|
meck:expect(ehttpc, request, fun(_, _, Req, _, _) -> {ok, 200, Req} end),
|
||||||
|
meck:expect(ehttpc_pool, pick_worker, 1, self()),
|
||||||
|
[ehttpc_sup, ehttpc, ehttpc_pool]
|
||||||
|
end,
|
||||||
|
fun meck:unload/1, fun(_) ->
|
||||||
|
Config = #{
|
||||||
|
base_url => #{
|
||||||
|
scheme => http,
|
||||||
|
host => "localhost",
|
||||||
|
port => 18083,
|
||||||
|
path => "/status"
|
||||||
|
},
|
||||||
|
connect_timeout => 1000,
|
||||||
|
pool_type => random,
|
||||||
|
pool_size => 1,
|
||||||
|
request => #{
|
||||||
|
method => get,
|
||||||
|
path => "/status",
|
||||||
|
headers => auth_headers()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ok, #{request := #{headers := Headers}} = State} = emqx_connector_http:on_start(
|
||||||
|
<<"test">>, Config
|
||||||
|
),
|
||||||
|
{ok, 200, Req} = emqx_connector_http:on_query(foo, {send_message, #{}}, State),
|
||||||
|
Tests =
|
||||||
|
[
|
||||||
|
?_assert(is_wrapped(V))
|
||||||
|
|| H <- Headers, is_tuple({K, V} = H), is_auth_header(untmpl(K))
|
||||||
|
],
|
||||||
|
[
|
||||||
|
?_assertEqual(4, length(Tests)),
|
||||||
|
?_assert(is_unwrapped_headers(element(2, Req)))
|
||||||
|
| Tests
|
||||||
|
]
|
||||||
|
end}.
|
||||||
|
|
||||||
|
auth_headers() ->
|
||||||
|
[
|
||||||
|
{<<"Authorization">>, ?MY_SECRET},
|
||||||
|
{<<"authorization">>, ?MY_SECRET},
|
||||||
|
{<<"Proxy-Authorization">>, ?MY_SECRET},
|
||||||
|
{<<"proxy-authorization">>, ?MY_SECRET},
|
||||||
|
{<<"X-Custom-Header">>, <<"foobar">>}
|
||||||
|
].
|
||||||
|
|
||||||
|
is_auth_header(<<"Authorization">>) -> true;
|
||||||
|
is_auth_header(<<"Proxy-Authorization">>) -> true;
|
||||||
|
is_auth_header(<<"authorization">>) -> true;
|
||||||
|
is_auth_header(<<"proxy-authorization">>) -> true;
|
||||||
|
is_auth_header(_Other) -> false.
|
||||||
|
|
||||||
|
is_wrapped(Secret) when is_function(Secret) ->
|
||||||
|
untmpl(emqx_secret:unwrap(Secret)) =:= ?MY_SECRET;
|
||||||
|
is_wrapped(_Other) ->
|
||||||
|
false.
|
||||||
|
|
||||||
|
untmpl([{_, V} | _]) -> V.
|
||||||
|
|
||||||
|
is_unwrapped_headers(Headers) ->
|
||||||
|
lists:all(fun is_unwrapped_header/1, Headers).
|
||||||
|
|
||||||
|
is_unwrapped_header({_, V}) when is_function(V) -> false;
|
||||||
|
is_unwrapped_header({_, [{str, _V}]}) -> throw(unexpected_tmpl_token);
|
||||||
|
is_unwrapped_header(_) -> true.
|
|
@ -0,0 +1 @@
|
||||||
|
Wrap potentially sensitive data in `emqx_connector_http` if `Authorization` headers are being passed at initialization.
|
Loading…
Reference in New Issue