diff --git a/apps/emqx/etc/emqx_cloud/vm.args b/apps/emqx/etc/emqx_cloud/vm.args index 0ee4b1e15..8111eed38 100644 --- a/apps/emqx/etc/emqx_cloud/vm.args +++ b/apps/emqx/etc/emqx_cloud/vm.args @@ -36,6 +36,11 @@ ## Can be one of: inet_tcp, inet6_tcp, inet_tls #-proto_dist inet_tcp +## The shell is started in a restricted mode. +## In this mode, the shell evaluates a function call only if allowed. +## Prevent user from accidentally calling a function from the prompt that could harm a running system. +-stdlib restricted_shell emqx_restricted_shell + ## Specify SSL Options in the file if using SSL for Erlang Distribution. ## Used only when -proto_dist set to inet_tls #-ssl_dist_optfile {{ platform_etc_dir }}/ssl_dist.conf diff --git a/apps/emqx/etc/emqx_edge/vm.args b/apps/emqx/etc/emqx_edge/vm.args index 70ce81f9f..743068538 100644 --- a/apps/emqx/etc/emqx_edge/vm.args +++ b/apps/emqx/etc/emqx_edge/vm.args @@ -35,6 +35,11 @@ ## Can be one of: inet_tcp, inet6_tcp, inet_tls #-proto_dist inet_tcp +## The shell is started in a restricted mode. +## In this mode, the shell evaluates a function call only if allowed. +## Prevent user from accidentally calling a function from the prompt that could harm a running system. +-stdlib restricted_shell emqx_restricted_shell + ## Specify SSL Options in the file if using SSL for Erlang Distribution. ## Used only when -proto_dist set to inet_tls #-ssl_dist_optfile {{ platform_etc_dir }}/ssl_dist.conf diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index 163e3eb39..a249447d3 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -48,6 +48,8 @@ -export([post_config_update/5]). +-export([format_addr/1]). + -define(CONF_KEY_PATH, [listeners]). -define(TYPES_STRING, ["tcp","ssl","ws","wss","quic"]). @@ -363,7 +365,12 @@ merge_default(Options) -> end. format_addr(Port) when is_integer(Port) -> - io_lib:format("0.0.0.0:~w", [Port]); + io_lib:format(":~w", [Port]); +%% Print only the port number when bound on all interfaces +format_addr({{0, 0, 0, 0}, Port}) -> + format_addr(Port); +format_addr({{0, 0, 0, 0, 0, 0, 0, 0}, Port}) -> + format_addr(Port); format_addr({Addr, Port}) when is_list(Addr) -> io_lib:format("~ts:~w", [Addr, Port]); format_addr({Addr, Port}) when is_tuple(Addr) -> diff --git a/apps/emqx_dashboard/src/emqx_dashboard.erl b/apps/emqx_dashboard/src/emqx_dashboard.erl index 1777dbd86..37629c16e 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard.erl @@ -62,11 +62,11 @@ start_listeners() -> middlewares => [cowboy_router, ?EMQX_MIDDLE, cowboy_handler] }, Res = - lists:foldl(fun({Name, Protocol, Port, RanchOptions}, Acc) -> + lists:foldl(fun({Name, Protocol, Bind, RanchOptions}, Acc) -> Minirest = BaseMinirest#{protocol => Protocol}, case minirest:start(Name, RanchOptions, Minirest) of {ok, _} -> - ?ULOG("Start listener ~ts on ~p successfully.~n", [Name, Port]), + ?ULOG("Start listener ~ts on ~ts successfully.~n", [Name, emqx_listeners:format_addr(Bind)]), Acc; {error, _Reason} -> %% Don't record the reason because minirest already does(too much logs noise). @@ -82,7 +82,7 @@ stop_listeners() -> [begin case minirest:stop(Name) of ok -> - ?ULOG("Stop listener ~ts on ~p successfully.~n", [Name, Port]); + ?ULOG("Stop listener ~ts on ~ts successfully.~n", [Name, emqx_listeners:format_addr(Port)]); {error, not_found} -> ?SLOG(warning, #{msg => "stop_listener_failed", name => Name, port => Port}) end @@ -101,17 +101,17 @@ apps() -> listeners() -> [begin Protocol = maps:get(protocol, ListenerOption0, http), - {ListenerOption, Port} = ip_port(ListenerOption0), + {ListenerOption, Bind} = ip_port(ListenerOption0), Name = listener_name(Protocol, ListenerOption), RanchOptions = ranch_opts(maps:without([protocol], ListenerOption)), - {Name, Protocol, Port, RanchOptions} + {Name, Protocol, Bind, RanchOptions} end || ListenerOption0 <- emqx_conf:get([dashboard, listeners], [])]. ip_port(Opts) -> ip_port(maps:take(bind, Opts), Opts). ip_port(error, Opts) -> {Opts#{port => 18083}, 18083}; ip_port({Port, Opts}, _) when is_integer(Port) -> {Opts#{port => Port}, Port}; -ip_port({{IP, Port}, Opts}, _) -> {Opts#{port => Port, ip => IP}, Port}. +ip_port({{IP, Port}, Opts}, _) -> {Opts#{port => Port, ip => IP}, {IP, Port}}. ranch_opts(RanchOptions) -> diff --git a/apps/emqx_machine/src/emqx_machine_app.erl b/apps/emqx_machine/src/emqx_machine_app.erl index 9e96ea234..ae837db2c 100644 --- a/apps/emqx_machine/src/emqx_machine_app.erl +++ b/apps/emqx_machine/src/emqx_machine_app.erl @@ -24,6 +24,7 @@ start(_Type, _Args) -> ok = emqx_machine:start(), + _ = emqx_restricted_shell:set_prompt_func(), emqx_machine_sup:start_link(). stop(_State) -> diff --git a/apps/emqx_machine/src/emqx_restricted_shell.erl b/apps/emqx_machine/src/emqx_restricted_shell.erl new file mode 100644 index 000000000..d855b7aff --- /dev/null +++ b/apps/emqx_machine/src/emqx_restricted_shell.erl @@ -0,0 +1,112 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021-2022 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_restricted_shell). + +-export([local_allowed/3, non_local_allowed/3]). +-export([set_prompt_func/0, prompt_func/1]). +-export([lock/0, unlock/0, is_locked/0]). + +-include_lib("emqx/include/logger.hrl"). + +-define(APP, 'emqx_machine'). +-define(IS_LOCKED, 'restricted.is_locked'). +-define(MAX_HEAP_SIZE, 1024 * 1024 * 1). +-define(MAX_ARGS_SIZE, 1024 * 10). + +-define(RED_BG, "\e[48;2;184;0;0m"). +-define(RESET, "\e[0m"). + +-define(LOCAL_PROHIBITED, [halt, q]). +-define(REMOTE_PROHIBITED, [{erlang, halt}, {c, q}, {init, stop}, {init, restart}, {init, reboot}]). + +is_locked() -> + {ok, false} =/= application:get_env(?APP, ?IS_LOCKED). + +lock() -> application:set_env(?APP, ?IS_LOCKED, true). +unlock() -> application:set_env(?APP, ?IS_LOCKED, false). + +set_prompt_func() -> + shell:prompt_func({?MODULE, prompt_func}). + +prompt_func(PropList) -> + Line = proplists:get_value(history, PropList, 1), + Version = emqx_release:version(), + case is_alive() of + true -> io_lib:format(<<"~ts(~s)~w> ">>, [Version, node(), Line]); + false -> io_lib:format(<<"~ts ~w> ">>, [Version, Line]) + end. + +local_allowed(MF, Args, State) -> + Allowed = check_allowed(MF, ?LOCAL_PROHIBITED), + log(Allowed, MF, Args), + {is_allowed(Allowed), State}. + +non_local_allowed(MF, Args, State) -> + Allow = check_allowed(MF, ?REMOTE_PROHIBITED), + log(Allow, MF, Args), + {is_allowed(Allow), State}. + +check_allowed(MF, NotAllowed) -> + case {lists:member(MF, NotAllowed), is_locked()} of + {true, false} -> exempted; + {true, true} -> prohibited; + {false, _} -> ignore + end. + +is_allowed(prohibited) -> false; +is_allowed(_) -> true. + +limit_warning(MF, Args) -> + max_heap_size_warning(MF, Args), + max_args_warning(MF, Args). + +max_args_warning(MF, Args) -> + ArgsSize = erts_debug:flat_size(Args), + case ArgsSize < ?MAX_ARGS_SIZE of + true -> ok; + false -> + warning("[WARNING] current_args_size:~w, max_args_size:~w", [ArgsSize, ?MAX_ARGS_SIZE]), + ?SLOG(warning, #{msg => "execute_function_in_shell_max_args_size", + function => MF, + args => Args, + args_size => ArgsSize, + max_heap_size => ?MAX_ARGS_SIZE}) + end. + +max_heap_size_warning(MF, Args) -> + {heap_size, HeapSize} = erlang:process_info(self(), heap_size), + case HeapSize < ?MAX_HEAP_SIZE of + true -> ok; + false -> + warning("[WARNING] current_heap_size:~w, max_heap_size_warning:~w", [HeapSize, ?MAX_HEAP_SIZE]), + ?SLOG(warning, #{msg => "shell_process_exceed_max_heap_size", + current_heap_size => HeapSize, + function => MF, + args => Args, + max_heap_size => ?MAX_HEAP_SIZE}) + end. + +log(prohibited, MF, Args) -> + warning("DANGEROUS FUNCTION: FORBIDDEN IN SHELL!!!!!", []), + ?SLOG(error, #{msg => "execute_function_in_shell_prohibited", function => MF, args => Args}); +log(exempted, MF, Args) -> + limit_warning(MF, Args), + ?SLOG(error, #{msg => "execute_dangerous_function_in_shell_exempted", function => MF, args => Args}); +log(ignore, MF, Args) -> limit_warning(MF, Args). + +warning(Format, Args) -> + io:format(?RED_BG ++ Format ++ ?RESET ++ "~n", Args). diff --git a/apps/emqx_machine/src/user_default.erl b/apps/emqx_machine/src/user_default.erl new file mode 100644 index 000000000..91899be61 --- /dev/null +++ b/apps/emqx_machine/src/user_default.erl @@ -0,0 +1,31 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021-2022 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(user_default). + +%% INCLUDE BEGIN +%% Import all the record definitions from the header file into the erlang shell. +-include_lib("emqx/include/emqx.hrl"). +-include_lib("emqx/include/logger.hrl"). +-include_lib("emqx/include/emqx_mqtt.hrl"). +-include_lib("emqx_conf/include/emqx_conf.hrl"). +-include_lib("emqx_dashboard/include/emqx_dashboard.hrl"). +%% INCLUDE END + +%% API +-export([lock/0, unlock/0]). + +lock() -> emqx_restricted_shell:lock(). +unlock() -> emqx_restricted_shell:unlock(). diff --git a/apps/emqx_machine/test/emqx_restricted_shell_SUITE.erl b/apps/emqx_machine/test/emqx_restricted_shell_SUITE.erl new file mode 100644 index 000000000..4ae0e66a2 --- /dev/null +++ b/apps/emqx_machine/test/emqx_restricted_shell_SUITE.erl @@ -0,0 +1,72 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2022 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_restricted_shell_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("emqx/include/emqx.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). + +all() -> emqx_common_test_helpers:all(?MODULE). + +init_per_suite(Config) -> + emqx_common_test_helpers:start_apps([]), + Config. + +end_per_suite(_Config) -> + emqx_common_test_helpers:stop_apps([]). + +t_local_allowed(_Config) -> + LocalProhibited = [halt, q], + State = undefined, + lists:foreach(fun(LocalFunc) -> + ?assertEqual({false, State}, emqx_restricted_shell:local_allowed(LocalFunc, [], State)) + end, LocalProhibited), + LocalAllowed = [ls, pwd], + lists:foreach(fun(LocalFunc) -> + ?assertEqual({true, State},emqx_restricted_shell:local_allowed(LocalFunc, [], State)) + end, LocalAllowed), + ok. + +t_non_local_allowed(_Config) -> + RemoteProhibited = [{erlang, halt}, {c, q}, {init, stop}, {init, restart}, {init, reboot}], + State = undefined, + lists:foreach(fun(RemoteFunc) -> + ?assertEqual({false, State}, emqx_restricted_shell:non_local_allowed(RemoteFunc, [], State)) + end, RemoteProhibited), + RemoteAllowed = [{erlang, date}, {erlang, system_time}], + lists:foreach(fun(RemoteFunc) -> + ?assertEqual({true, State}, emqx_restricted_shell:local_allowed(RemoteFunc, [], State)) + end, RemoteAllowed), + ok. + +t_lock(_Config) -> + State = undefined, + emqx_restricted_shell:lock(), + ?assertEqual({false, State}, emqx_restricted_shell:local_allowed(q, [], State)), + ?assertEqual({true, State}, emqx_restricted_shell:local_allowed(ls, [], State)), + ?assertEqual({false, State}, emqx_restricted_shell:non_local_allowed({init, stop}, [], State)), + ?assertEqual({true, State}, emqx_restricted_shell:non_local_allowed({inet, getifaddrs}, [], State)), + emqx_restricted_shell:unlock(), + ?assertEqual({true, State}, emqx_restricted_shell:local_allowed(q, [], State)), + ?assertEqual({true, State}, emqx_restricted_shell:local_allowed(ls, [], State)), + ?assertEqual({true, State}, emqx_restricted_shell:non_local_allowed({init, stop}, [], State)), + ?assertEqual({true, State}, emqx_restricted_shell:non_local_allowed({inet, getifaddrs}, [], State)), + emqx_restricted_shell:lock(), + ok.