From 746290cebbca1805685134788699f25369ad4fa4 Mon Sep 17 00:00:00 2001 From: Jakub Witczak Date: Fri, 20 Mar 2026 16:27:54 +0100 Subject: [PATCH 1/2] ssh: Change shell and exec defaults to disabled Change the shell and exec daemon options to default to disabled instead of enabling the Erlang shell and Erlang term evaluation respectively. This implements the "secure by default" principle, preventing authenticated users from executing arbitrary Erlang code unless explicitly configured. The new erlang_eval exec option enables Erlang term evaluation via exec requests, replacing the previous implicit behavior. Applications requiring these services must now explicitly enable them: %% Enable Erlang shell ssh:daemon(Port, [{shell, {shell, start, []}} | Options]) %% Enable Erlang term evaluation via exec ssh:daemon(Port, [{exec, erlang_eval} | Options]) Also fix a client-side crash in ssh:shell/1 where the missing exec key in options caused channel_cb_init_args/1 to append undefined to the callback init args. Both erlang_eval and undefined are now filtered out, as neither is a callback init argument. --- lib/kernel/test/shell_test_lib.erl | 7 +- lib/ssh/doc/guides/hardening.md | 7 +- lib/ssh/doc/guides/introduction.md | 9 ++- lib/ssh/doc/guides/using_ssh.md | 3 +- lib/ssh/src/ssh.hrl | 30 ++++++- lib/ssh/src/ssh_cli.erl | 41 +++++----- lib/ssh/src/ssh_client_channel.erl | 4 + lib/ssh/src/ssh_connection.erl | 10 ++- lib/ssh/src/ssh_options.erl | 5 +- lib/ssh/src/ssh_sftp.erl | 2 + lib/ssh/test/ssh_algorithms_SUITE.erl | 2 +- lib/ssh/test/ssh_basic_SUITE.erl | 112 +++++++++++++++++--------- lib/ssh/test/ssh_connection_SUITE.erl | 50 ++++++------ lib/ssh/test/ssh_options_SUITE.erl | 10 ++- lib/ssh/test/ssh_test_lib.erl | 8 +- lib/ssh/test/ssh_to_openssh_SUITE.erl | 12 ++- 16 files changed, 197 insertions(+), 115 deletions(-) diff --git a/lib/kernel/test/shell_test_lib.erl b/lib/kernel/test/shell_test_lib.erl index 84fdf1bd1ecb..a0f7453294be 100644 --- a/lib/kernel/test/shell_test_lib.erl +++ b/lib/kernel/test/shell_test_lib.erl @@ -182,9 +182,10 @@ setup_tty(Config) -> PrivDir = filename:join(proplists:get_value(priv_dir, Config), "nopubkey"), file:make_dir(PrivDir), SysDir = proplists:get_value(data_dir, Config), - {ok, _Sshd} = ssh:daemon(8989, [{system_dir, SysDir}, - {user_dir, PrivDir}, - {password, "bar"}]) + {ok, _Sshd} = ssh:daemon(8989, [{shell, {shell, start, []}}, + {system_dir, SysDir}, + {user_dir, PrivDir}, + {password, "bar"}]) end), os:cmd(os:find_executable("tmux") ++ " new-window -n " ++ ClientName ++ " -d -- "++ "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null localhost -p 8989 -l foo"), diff --git a/lib/ssh/doc/guides/hardening.md b/lib/ssh/doc/guides/hardening.md index e1a3da2881cb..ab1472179ed1 100644 --- a/lib/ssh/doc/guides/hardening.md +++ b/lib/ssh/doc/guides/hardening.md @@ -303,10 +303,9 @@ _exec_ server-side service takes a string provided by the client, evaluates it and returns the result. The _shell_ function enables the client to open a shell in the shell host. -Those service could - and should - be disabled when they are not needed. The -options [exec](`t:ssh:exec_daemon_option/0`) and -[shell](`t:ssh:shell_daemon_option/0`) are enabled per default but could be set -to `disabled` if not needed. The same options could also install handlers for +The options [exec](`t:ssh:exec_daemon_option/0`) and +[shell](`t:ssh:shell_daemon_option/0`) are disabled per default. +The same options could also install handlers for the string(s) passed from the client to the server. ### The id string diff --git a/lib/ssh/doc/guides/introduction.md b/lib/ssh/doc/guides/introduction.md index 44be5dae98ae..4cc5e28abfaa 100644 --- a/lib/ssh/doc/guides/introduction.md +++ b/lib/ssh/doc/guides/introduction.md @@ -117,12 +117,13 @@ Channels come in the following three flavors: as SFTP [(ssh_sftpd)](`m:ssh_sftpd`), that is built into the SSH daemon (server) by default, but it can be disabled. The Erlang `ssh` daemon can be configured to run any Erlang- implemented SSH subsystem. -- _Shell_ \- Interactive shell. By default the Erlang daemon runs the Erlang - shell. The shell can be customized by providing your own read-eval-print loop. +- _Shell_ \- Interactive shell. By default the Erlang daemon does not expose the Erlang + shell. It can be enabled with option `{shell, {shell, start, []}}` + The shell can be customized by providing your own read-eval-print loop. You can also provide your own Command-Line Interface (CLI) implementation, but that is much more work. -- _Exec_ \- One-time remote execution of commands. See function - `ssh_connection:exec/4` for more information. +- _Exec_ \- By default one-time remote execution of commands is disabled. + See function `ssh_connection:exec/4` for more information. ## Where to Find More Information diff --git a/lib/ssh/doc/guides/using_ssh.md b/lib/ssh/doc/guides/using_ssh.md index a1b76ce63b21..983ab7af2c2f 100644 --- a/lib/ssh/doc/guides/using_ssh.md +++ b/lib/ssh/doc/guides/using_ssh.md @@ -83,7 +83,8 @@ _Step 3._ Start the Erlang `ssh` daemon: 1> ssh:start(). ok 2> {ok, Sshd} = ssh:daemon(8989, [{system_dir, "/tmp/ssh_daemon"}, - {user_dir, "/tmp/otptest_user/.ssh"}]). + {user_dir, "/tmp/otptest_user/.ssh"}, + {shell, {shell, start, []}}]). {ok,<0.54.0>} 3> ``` diff --git a/lib/ssh/src/ssh.hrl b/lib/ssh/src/ssh.hrl index f9e359bfb0ba..604f250e2fbf 100644 --- a/lib/ssh/src/ssh.hrl +++ b/lib/ssh/src/ssh.hrl @@ -36,8 +36,6 @@ -define(DEFAULT_TRANSPORT, {tcp, gen_tcp, tcp_closed} ). --define(DEFAULT_SHELL, {shell, start, []} ). - -define(DEFAULT_TIMEOUT, 5000). -define(MAX_RND_PADDING_LEN, 15). @@ -842,6 +840,14 @@ risk. -type shell_daemon_option() :: {shell, shell_spec()} . -doc(#{group => <<"Daemon Options">>}). -type shell_spec() :: mod_fun_args() | shell_fun() | disabled . +-doc """ +The default is `disabled`. + +To enable the Erlang shell (the behavior from OTP versions prior to OTP @OTP-19969@): +``` +ssh:daemon(Port, [{shell, {shell, start, []}} | Options]) +``` +""". -doc(#{group => <<"Daemon Options">>, equiv => 'shell_fun/2'/0}). -type shell_fun() :: 'shell_fun/1'() | 'shell_fun/2'() . @@ -850,7 +856,7 @@ risk. -type 'shell_fun/1'() :: fun((User::string()) -> pid()) . -doc """ Defines the read-eval-print loop used in a daemon when a shell is requested by -the client. The default is to use the Erlang shell: `{shell, start, []}` +the client. See the option [`exec-option`](`t:exec_daemon_option/0`) for a description of how the daemon executes shell-requests and exec-requests depending on the shell- @@ -862,7 +868,23 @@ and exec-options. -doc(#{group => <<"Daemon Options">>}). -type exec_daemon_option() :: {exec, exec_spec()} . -doc(#{group => <<"Daemon Options">>}). --type exec_spec() :: {direct, exec_fun()} | disabled | deprecated_exec_opt(). +-type exec_spec() :: {direct, exec_fun()} | disabled | deprecated_exec_opt() | erlang_eval. +-doc """ +The default is `disabled`. + +Value `erlang_eval` enables evaluation of Erlang terms via exec requests. +This works when the shell option is either `disabled` (no shell) or +`{shell, start, []}` (Erlang shell). It does not work with custom shells. + +To restore the behavior from OTP versions prior to OTP @OTP-19969@, configure: +``` +ssh:daemon(Port, [{shell, {shell, start, []}}, + {exec, erlang_eval} + | Options]) +``` + +For new code, consider using `{direct, Fun}` for more controlled exec handling. +""". -doc(#{group => <<"Daemon Options">>}). -type exec_fun() :: 'exec_fun/1'() | 'exec_fun/2'() | 'exec_fun/3'(). -doc(#{group => <<"Daemon Options">>}). diff --git a/lib/ssh/src/ssh_cli.erl b/lib/ssh/src/ssh_cli.erl index 0c4682bff097..f0438aa7819f 100644 --- a/lib/ssh/src/ssh_cli.erl +++ b/lib/ssh/src/ssh_cli.erl @@ -41,19 +41,20 @@ %% state -record(state, { - cm, - channel, - pty, - encoding, - deduced_encoding, % OpenSSH sometimes lies about its encodeing. This variable - % is for the process of guessing the peer encoding, taylord - % after the behaviour of openssh. If it says latin1 it is so. - % It there arrives characters encoded in latin1 it is so. Otherwise - % assume utf8 until otherwise is proved. - group, - shell, - exec, - tty + cm, + channel, + pty, + encoding, + %% OpenSSH sometimes lies about its encodeing. This variable + %% is for the process of guessing the peer encoding, taylord + %% after the behaviour of openssh. If it says latin1 it is so. + %% It there arrives characters encoded in latin1 it is so. Otherwise + %% assume utf8 until otherwise is proved. + deduced_encoding, + group, + shell, + exec = erlang_eval, + tty }). @@ -212,16 +213,16 @@ handle_ssh_msg({ssh_cm, ConnectionHandler, {exec, ChannelId, WantReply, Cmd0}}, %% The standard I/O is directed from/to the channel ChannelId. exec_direct(ConnectionHandler, ChannelId, Cmd, F, WantReply, S1); - undefined when S0#state.shell == ?DEFAULT_SHELL ; - S0#state.shell == disabled -> - %% Exec called and the shell is the default shell (= Erlang shell). - %% To be exact, eval the term as an Erlang term (but not using the - %% ?DEFAULT_SHELL directly). This disables banner, prompts and such. + erlang_eval when S0#state.shell == disabled; + S0#state.shell == {shell, start, []} -> + %% Exec called and the shell is the Erlang shell or disabled. + %% To be exact, eval the term as an Erlang term + %% This disables banner, prompts and such. %% The standard I/O is directed from/to the channel ChannelId. exec_in_erlang_default_shell(ConnectionHandler, ChannelId, Cmd, WantReply, S1); - undefined -> - %% Exec called, but the a shell other than the default shell is defined. + erlang_eval -> + %% Exec called, but the shell is custom (not Erlang shell). %% No new exec shell is defined, so don't execute! %% We don't know if it is intended to use the new shell or not. {"Prohibited.", ?EXEC_ERROR_STATUS, 1}; diff --git a/lib/ssh/src/ssh_client_channel.erl b/lib/ssh/src/ssh_client_channel.erl index a4f272d24379..e650a8fa999e 100644 --- a/lib/ssh/src/ssh_client_channel.erl +++ b/lib/ssh/src/ssh_client_channel.erl @@ -420,7 +420,11 @@ init([Options]) -> channel_cb_init_args(Options) -> case proplists:get_value(exec, Options) of + erlang_eval -> + %% erlang_eval is a mode flag for ssh_cli, not a cb init arg + proplists:get_value(init_args, Options); undefined -> + %% exec key absent from Options (client-side ssh:shell/1) proplists:get_value(init_args, Options); Exec -> proplists:get_value(init_args, Options) ++ [Exec] diff --git a/lib/ssh/src/ssh_connection.erl b/lib/ssh/src/ssh_connection.erl index ab7a7972ac6d..d16a1e2fa153 100644 --- a/lib/ssh/src/ssh_connection.erl +++ b/lib/ssh/src/ssh_connection.erl @@ -50,7 +50,6 @@ these messages are handled by -include("ssh.hrl"). -include("ssh_connect.hrl"). -include("ssh_transport.hrl"). - %% API -export([session_channel/2, session_channel/4, exec/4, shell/2, subsystem/4, send/3, send/4, send/5, @@ -1497,7 +1496,8 @@ start_cli(#connection{options = Options, no_cli -> {error, cli_disabled}; {CbModule, Args} -> - ssh_connection_sup:start_channel(server, ConnectionSup, self(), CbModule, ChannelId, Args, Exec, Options) + ssh_connection_sup:start_channel(server, ConnectionSup, self(), + CbModule, ChannelId, Args, Exec, Options) end. @@ -1507,7 +1507,11 @@ start_subsystem(BinName, #connection{options = Options, Name = binary_to_list(BinName), case check_subsystem(Name, Options) of {Callback, Opts} when is_atom(Callback), Callback =/= none -> - ssh_connection_sup:start_channel(server, ConnectionSup, self(), Callback, ChannelId, Opts, undefined, Options); + %% Exec is not used by subsystems; undefined is filtered + %% out by channel_cb_init_args/1 so it won't be appended + %% to the callback's init args. + ssh_connection_sup:start_channel(server, ConnectionSup, self(), + Callback, ChannelId, Opts, undefined, Options); {none, _} -> {error, bad_subsystem}; {_, _} -> diff --git a/lib/ssh/src/ssh_options.erl b/lib/ssh/src/ssh_options.erl index 5d6df1efc3c0..b100874a5ac2 100644 --- a/lib/ssh/src/ssh_options.erl +++ b/lib/ssh/src/ssh_options.erl @@ -421,7 +421,7 @@ default(server) -> }, shell => - #{default => ?DEFAULT_SHELL, + #{default => disabled, chk => fun({M,F,A}) -> is_atom(M) andalso is_atom(F) andalso is_list(A); (disabled) -> true; (V) -> check_function1(V) orelse @@ -431,9 +431,10 @@ default(server) -> }, exec => - #{default => undefined, + #{default => disabled, chk => fun({direct, V}) -> check_function1(V) orelse check_function2(V) orelse check_function3(V); (disabled) -> true; + (erlang_eval) -> true; % Enable Erlang term evaluation %% Compatibility (undocumented): ({M,F,A}) -> is_atom(M) andalso is_atom(F) andalso is_list(A); (V) -> check_function1(V) orelse check_function2(V) orelse check_function3(V) diff --git a/lib/ssh/src/ssh_sftp.erl b/lib/ssh/src/ssh_sftp.erl index b54687d1ebdc..634376519498 100644 --- a/lib/ssh/src/ssh_sftp.erl +++ b/lib/ssh/src/ssh_sftp.erl @@ -172,6 +172,8 @@ start_channel(Cm, UserOptions0) when is_pid(Cm) -> PacketSize = proplists:get_value(packet_size, ChanOpts, ?XFER_PACKET_SIZE), case ssh_connection:session_channel(Cm, WindowSize, PacketSize, Timeout) of {ok, ChannelId} -> + %% Exec is not used by SFTP channels; undefined is filtered + %% out by channel_cb_init_args/1. case ssh_connection_handler:start_channel(Cm, ?MODULE, ChannelId, [Cm,ChannelId,SftpOpts], undefined) of {ok, Pid} -> diff --git a/lib/ssh/test/ssh_algorithms_SUITE.erl b/lib/ssh/test/ssh_algorithms_SUITE.erl index dc82ea4370cf..a47f46ad147e 100644 --- a/lib/ssh/test/ssh_algorithms_SUITE.erl +++ b/lib/ssh/test/ssh_algorithms_SUITE.erl @@ -231,7 +231,7 @@ init_per_group(Tag, Algs, Alg, PA, Config) -> end; _ -> - start_std_daemon([PrefAlgs], + start_std_daemon([{exec, erlang_eval}] ++ [PrefAlgs], [{pref_algs,PrefAlgs}, {tag_alg,{Tag,[Alg]}} | Config]) diff --git a/lib/ssh/test/ssh_basic_SUITE.erl b/lib/ssh/test/ssh_basic_SUITE.erl index 32ddad6564f9..5ae5523e6ce6 100644 --- a/lib/ssh/test/ssh_basic_SUITE.erl +++ b/lib/ssh/test/ssh_basic_SUITE.erl @@ -88,6 +88,7 @@ parallel_login/1, setopts_getopts/1, shell/1, + shell_disabled/1, shell_exit_status/1, shell_no_unicode/1, shell_socket/1, @@ -157,7 +158,7 @@ groups() -> max_initial_idle_time, openssh_zlib_basic_test, misc_ssh_options, inet_option, inet6_option, - shell, shell_socket, shell_ssh_conn, + shell, shell_disabled, shell_socket, shell_ssh_conn, shell_no_unicode, shell_unicode_string, close]}]. @@ -199,10 +200,11 @@ init_per_testcase(TestCase, Config) PrivDir = proplists:get_value(priv_dir, Config), UserDir = proplists:get_value(priv_dir, Config), SysDir = proplists:get_value(data_dir, Config), - Sftpd = {_Pid, _Host, Port} = - ssh_test_lib:daemon([{system_dir, SysDir}, - {user_dir, PrivDir}, - {user_passwords, [{"foo", "bar"}]}]), + Sftpd = {_Pid, _Host, Port} = + ssh_test_lib:daemon([{shell, {shell, start, []}}, + {system_dir, SysDir}, + {user_dir, PrivDir}, + {user_passwords, [{"foo", "bar"}]}]), ct:sleep(500), IO = ssh_test_lib:start_io_server(), Shell = ssh_test_lib:start_shell(Port, IO, [{user_dir,UserDir}, @@ -311,9 +313,10 @@ exec(Config) when is_list(Config) -> SystemDir = filename:join(proplists:get_value(priv_dir, Config), system), UserDir = proplists:get_value(priv_dir, Config), - {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir}, - {user_dir, UserDir}, - {failfun, fun ssh_test_lib:failfun/2}]), + {Pid, Host, Port} = ssh_test_lib:daemon([{exec, erlang_eval}, + {system_dir, SystemDir}, + {user_dir, UserDir}, + {failfun, fun ssh_test_lib:failfun/2}]), ConnectionRef = ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true}, {user_dir, UserDir}, @@ -354,9 +357,10 @@ exec_with_io_out(Config) when is_list(Config) -> SystemDir = filename:join(proplists:get_value(priv_dir, Config), system), UserDir = proplists:get_value(priv_dir, Config), - {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir}, - {user_dir, UserDir}, - {failfun, fun ssh_test_lib:failfun/2}]), + {Pid, Host, Port} = ssh_test_lib:daemon([{exec, erlang_eval}, + {system_dir, SystemDir}, + {user_dir, UserDir}, + {failfun, fun ssh_test_lib:failfun/2}]), ConnectionRef = ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true}, {user_dir, UserDir}, @@ -384,9 +388,10 @@ exec_with_io_in(Config) when is_list(Config) -> SystemDir = filename:join(proplists:get_value(priv_dir, Config), system), UserDir = proplists:get_value(priv_dir, Config), - {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir}, - {user_dir, UserDir}, - {failfun, fun ssh_test_lib:failfun/2}]), + {Pid, Host, Port} = ssh_test_lib:daemon([{exec, erlang_eval}, + {system_dir, SystemDir}, + {user_dir, UserDir}, + {failfun, fun ssh_test_lib:failfun/2}]), C = ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true}, {user_dir, UserDir}, {user_interaction, false}]), @@ -418,7 +423,8 @@ exec_compressed_helper(Config, CompressAlgorithm) -> SystemDir = filename:join(proplists:get_value(priv_dir, Config), system), UserDir = proplists:get_value(priv_dir, Config), - {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir},{user_dir, UserDir}, + {Pid, Host, Port} = ssh_test_lib:daemon([{exec, erlang_eval}, + {system_dir, SystemDir},{user_dir, UserDir}, {preferred_algorithms,[{compression, [CompressAlgorithm]}]}, {failfun, fun ssh_test_lib:failfun/2}]), @@ -452,7 +458,8 @@ idle_time_common(DaemonExtraOpts, ClientExtraOpts, Config) -> SystemDir = filename:join(proplists:get_value(priv_dir, Config), system), UserDir = proplists:get_value(priv_dir, Config), - {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir}, + {Pid, Host, Port} = ssh_test_lib:daemon([{exec, erlang_eval}, + {system_dir, SystemDir}, {user_dir, UserDir}, {failfun, fun ssh_test_lib:failfun/2} | DaemonExtraOpts @@ -507,24 +514,51 @@ shell(Config) when is_list(Config) -> process_flag(trap_exit, true), SystemDir = filename:join(proplists:get_value(priv_dir, Config), system), UserDir = proplists:get_value(priv_dir, Config), - - {_Pid, _Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir},{user_dir, UserDir}, - {failfun, fun ssh_test_lib:failfun/2}]), + {_Pid, _Host, Port} = ssh_test_lib:daemon([{shell, {shell, start, []}}, + {system_dir, SystemDir},{user_dir, UserDir}, + {failfun, fun ssh_test_lib:failfun/2}]), ct:sleep(500), - IO = ssh_test_lib:start_io_server(), Shell = ssh_test_lib:start_shell(Port, IO, [{user_dir,UserDir}]), receive {'EXIT', _, _} = Exit -> ct:log("~p:~p ~p", [?MODULE,?LINE,Exit]), - ct:fail(no_ssh_connection); + ct:fail(no_ssh_connection); ErlShellStart -> ct:log("Erlang shell start: ~p~n", [ErlShellStart]), do_shell(IO, Shell) - after + after 30000 -> ct:fail("timeout ~p:~p",[?MODULE,?LINE]) end. - + +shell_disabled(Config) when is_list(Config) -> + process_flag(trap_exit, true), + SystemDir = filename:join(proplists:get_value(priv_dir, Config), system), + UserDir = proplists:get_value(priv_dir, Config), + {_Pid, _Host, Port} = ssh_test_lib:daemon([{shell, disabled}, + {system_dir, SystemDir},{user_dir, UserDir}, + {failfun, fun ssh_test_lib:failfun/2}]), + ct:sleep(500), + IO = ssh_test_lib:start_io_server(), + Shell = ssh_test_lib:start_shell(Port, IO, [{user_dir,UserDir}]), + ?CT_LOG("Shell = ~p", [Shell]), + ExpectedMessages = + [<<"Prohibited.">>, + <<"Connection closed by peer">>, + <<"Status: 255">>, + {'EXIT', Shell, normal}], + [receive + Msg -> + ?CT_LOG("Received msg = ~p", [Msg]), + ok + after + 3000 -> + ?CT_LOG("Missing msg = ~p", [Msg]), + ?CT_LOG("Message queue of ~p:~n~p", + [self(), erlang:process_info(self(), messages)]), + ct:fail("timeout ~p:~p",[?MODULE,?LINE]) + end || Msg <- ExpectedMessages]. + %%-------------------------------------------------------------------- %%% Test that ssh:shell/2 works when attaching to a open TCP-connection shell_socket(Config) when is_list(Config) -> @@ -532,8 +566,9 @@ shell_socket(Config) when is_list(Config) -> SystemDir = filename:join(proplists:get_value(priv_dir, Config), system), UserDir = proplists:get_value(priv_dir, Config), - {_Pid, Host0, Port} = ssh_test_lib:daemon([{system_dir, SystemDir},{user_dir, UserDir}, - {failfun, fun ssh_test_lib:failfun/2}]), + {_Pid, Host0, Port} = ssh_test_lib:daemon([{shell, {shell, start, []}}, + {system_dir, SystemDir},{user_dir, UserDir}, + {failfun, fun ssh_test_lib:failfun/2}]), Host = ssh_test_lib:mangle_connect_address(Host0), ct:sleep(500), @@ -576,8 +611,9 @@ shell_ssh_conn(Config) when is_list(Config) -> SystemDir = filename:join(proplists:get_value(priv_dir, Config), system), UserDir = proplists:get_value(priv_dir, Config), - {_Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir},{user_dir, UserDir}, - {failfun, fun ssh_test_lib:failfun/2}]), + {_Pid, Host, Port} = ssh_test_lib:daemon([{shell, {shell, start, []}}, + {system_dir, SystemDir},{user_dir, UserDir}, + {failfun, fun ssh_test_lib:failfun/2}]), ct:sleep(500), IO = ssh_test_lib:start_io_server(), @@ -606,11 +642,12 @@ cli(Config) when is_list(Config) -> ok = ssh_test_lib:del_dirs(TmpDir), ok = file:make_dir(TmpDir), - {_Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir},{user_dir, UserDir}, - {password, "morot"}, - {ssh_cli, {ssh_test_cli, [cli,TmpDir]}}, - {subsystems, []}, - {failfun, fun ssh_test_lib:failfun/2}]), + {_Pid, Host, Port} = ssh_test_lib:daemon([{exec, erlang_eval}, + {system_dir, SystemDir},{user_dir, UserDir}, + {password, "morot"}, + {ssh_cli, {ssh_test_cli, [cli,TmpDir]}}, + {subsystems, []}, + {failfun, fun ssh_test_lib:failfun/2}]), ct:sleep(500), ConnectionRef = ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true}, @@ -1077,12 +1114,11 @@ peername_sockname(Config) when is_list(Config) -> SystemDir = filename:join(proplists:get_value(priv_dir, Config), system), UserDir = proplists:get_value(priv_dir, Config), - {_Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir}, - {user_dir, UserDir}, - {subsystems, [{"peername_sockname", - {ssh_peername_sockname_server, []}} - ]} - ]), + {_Pid, Host, Port} = ssh_test_lib:daemon([{exec, erlang_eval}, + {system_dir, SystemDir}, + {user_dir, UserDir}, + {subsystems, [{"peername_sockname", + {ssh_peername_sockname_server, []}}]}]), ConnectionRef = ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true}, {user_dir, UserDir}, diff --git a/lib/ssh/test/ssh_connection_SUITE.erl b/lib/ssh/test/ssh_connection_SUITE.erl index 8e29485a6475..2d2569bd3f9b 100644 --- a/lib/ssh/test/ssh_connection_SUITE.erl +++ b/lib/ssh/test/ssh_connection_SUITE.erl @@ -427,7 +427,8 @@ daemon_start(Config) -> {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir}, {user_dir, UserDir}, {password, "morot"}, - {exec, fun ssh_exec_echo/1}]), + {exec, fun ssh_exec_echo/1}, + {shell, {shell, start, []}}]), {Pid, Host, Port, UserDir}. %%-------------------------------------------------------------------- @@ -891,9 +892,10 @@ start_shell_exec(Config) when is_list(Config) -> file:make_dir(UserDir), SysDir = proplists:get_value(data_dir, Config), {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir}, - {user_dir, UserDir}, - {password, "morot"}, - {exec, {?MODULE,ssh_exec_echo,["foo"]}} ]), + {user_dir, UserDir}, + {password, "morot"}, + {exec, {?MODULE,ssh_exec_echo,["foo"]}}, + {shell, {shell, start, []}}]), ConnectionRef = ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true}, {user, "foo"}, @@ -901,7 +903,7 @@ start_shell_exec(Config) when is_list(Config) -> {user_interaction, true}, {user_dir, UserDir}]), test_shell_is_enabled(ConnectionRef), - test_exec_is_enabled(ConnectionRef, "testing", <<"echo testing\r\n">>), + test_exec_is_enabled(ConnectionRef, "testing", <<"echo testing foo\r\n">>), ssh:close(ConnectionRef), ssh:stop_daemon(Pid). @@ -912,9 +914,10 @@ exec_erlang_term(Config) when is_list(Config) -> file:make_dir(UserDir), SysDir = proplists:get_value(data_dir, Config), {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir}, - {user_dir, UserDir}, - {password, "morot"} - ]), + {user_dir, UserDir}, + {password, "morot"}, + {shell, {shell, start, []}}, + {exec, erlang_eval}]), ConnectionRef = ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true}, {user, "foo"}, @@ -955,10 +958,10 @@ exec_disabled(Config) when is_list(Config) -> file:make_dir(UserDir), SysDir = proplists:get_value(data_dir, Config), {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir}, - {user_dir, UserDir}, - {password, "morot"}, - {exec, disabled} - ]), + {user_dir, UserDir}, + {password, "morot"}, + {exec, disabled}, + {shell, {shell, start, []}}]), ConnectionRef = ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true}, {user, "foo"}, {password, "morot"}, @@ -978,7 +981,8 @@ exec_shell_disabled(Config) when is_list(Config) -> {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir}, {user_dir, UserDir}, {password, "morot"}, - {shell, disabled} + {shell, disabled}, + {exec, erlang_eval} ]), ConnectionRef = ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true}, {user, "foo"}, @@ -1768,11 +1772,12 @@ max_channels_option(Config) when is_list(Config) -> file:make_dir(UserDir), SysDir = proplists:get_value(data_dir, Config), {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir}, - {user_dir, UserDir}, - {password, "morot"}, - {max_channels, 3}, - {subsystems, [{"echo_n", {ssh_echo_server, [4000000]}}]} - ]), + {user_dir, UserDir}, + {password, "morot"}, + {max_channels, 3}, + {subsystems, [{"echo_n", {ssh_echo_server, [4000000]}}]}, + {shell, {shell, start, []}}, + {exec, erlang_eval}]), ConnectionRef = ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true}, {user, "foo"}, @@ -2131,12 +2136,11 @@ test_exec_is_enabled(ConnectionRef, Exec, Expect) -> <>}} = R -> ct:log("~p:~p Got expected ~p",[?MODULE,?LINE,R]); Other -> - ct:log("~p:~p Got unexpected ~p~nExpect: ~p~n", - [?MODULE,?LINE, Other, {ssh_cm, ConnectionRef, - {data, ChannelId, 0, Expect}}]), - {fail, "Unexpected data"} + ct:fail("Got unexpected ~p~nExpect: ~p~n", + [Other, {ssh_cm, ConnectionRef, + {data, ChannelId, 0, Expect}}]) after 5000 -> - {fail,"Exec Timeout"} + ct:fail("Exec Timeout") end. %%%---------------------------------------------------------------- diff --git a/lib/ssh/test/ssh_options_SUITE.erl b/lib/ssh/test/ssh_options_SUITE.erl index bc08c97498f6..3d6525535196 100644 --- a/lib/ssh/test/ssh_options_SUITE.erl +++ b/lib/ssh/test/ssh_options_SUITE.erl @@ -1397,9 +1397,10 @@ ssh_connect_nonegtimeout_connected(Config, Parallel) -> ct:log("Parallel: ~p",[Parallel]), {_Pid, _Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir},{user_dir, UserDir}, - {parallel_login, Parallel}, - {negotiation_timeout, NegTimeOut}, - {failfun, fun ssh_test_lib:failfun/2}]), + {parallel_login, Parallel}, + {negotiation_timeout, NegTimeOut}, + {failfun, fun ssh_test_lib:failfun/2}, + {shell, {shell, start, []}}]), ct:log("~p Listen ~p:~p",[_Pid,_Host,Port]), ct:sleep(500), @@ -1981,7 +1982,8 @@ daemon_replace_options_algs_connect(Config) -> {Pid, Host, Port} = ssh_test_lib:std_daemon(Config, - [{preferred_algorithms,[{kex,[A1]}]} + [{exec, erlang_eval}, + {preferred_algorithms,[{kex,[A1]}]} ]), [A1] = get_preferred_algorithms(Pid, kex), diff --git a/lib/ssh/test/ssh_test_lib.erl b/lib/ssh/test/ssh_test_lib.erl index 116b5a0140da..fe6efccd4b97 100644 --- a/lib/ssh/test/ssh_test_lib.erl +++ b/lib/ssh/test/ssh_test_lib.erl @@ -463,19 +463,19 @@ rcv_expected(Expect, SshPort, Timeout) -> {SshPort, Recvd} when is_function(Expect) -> case Expect(Recvd) of true -> - ct:log("Got expected ~p from ~p",[Recvd,SshPort]), + ?CT_LOG("Got expected ~p from ~p",[Recvd,SshPort]), catch port_close(SshPort), rcv_lingering(50); false -> - ct:log("Got UNEXPECTED ~p~n",[Recvd]), + ?CT_LOG("Got UNEXPECTED ~p~n",[Recvd]), rcv_expected(Expect, SshPort, Timeout) end; {SshPort, Expect} -> - ct:log("Got expected ~p from ~p",[Expect,SshPort]), + ?CT_LOG("Got expected ~p from ~p",[Expect,SshPort]), catch port_close(SshPort), rcv_lingering(50); Other -> - ct:log("Got UNEXPECTED ~p~nExpect ~p",[Other, {SshPort,Expect}]), + ?CT_LOG("Got UNEXPECTED ~p~nExpect ~p",[Other, {SshPort,Expect}]), rcv_expected(Expect, SshPort, Timeout) after Timeout -> diff --git a/lib/ssh/test/ssh_to_openssh_SUITE.erl b/lib/ssh/test/ssh_to_openssh_SUITE.erl index da4bcafdb624..258a341b80d8 100644 --- a/lib/ssh/test/ssh_to_openssh_SUITE.erl +++ b/lib/ssh/test/ssh_to_openssh_SUITE.erl @@ -206,6 +206,7 @@ eclient_oserver_helper2({Shell, Prev, IO}, Config) -> exec_with_io_in_sshc(Config) when is_list(Config) -> SystemDir = proplists:get_value(data_dir, Config), {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir}, + {exec, erlang_eval}, ?ALIVE, {failfun, fun ssh_test_lib:failfun/2}]), ct:sleep(500), @@ -302,7 +303,8 @@ eserver_oclient_renegotiate_helper1(Config) -> {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir}, {failfun, fun ssh_test_lib:failfun/2}, ?ALIVE, - {bannerfun, BannerFun}]), + {bannerfun, BannerFun}, + {shell, {shell, start, []}}]), ct:sleep(500), RenegLimitK = 3, @@ -368,9 +370,10 @@ tunnel_out_non_erlclient_erlserver(Config) -> _PrivDir = proplists:get_value(priv_dir, Config), {_Pid, Host, Port} = ssh_test_lib:daemon([{tcpip_tunnel_out, true}, - {system_dir, SystemDir}, + {system_dir, SystemDir}, ?ALIVE, - {failfun, fun ssh_test_lib:failfun/2}]), + {failfun, fun ssh_test_lib:failfun/2}, + {shell, {shell, start, []}}]), {ToSock, _ToHost, ToPort} = tunneling_listner(), ListenHost = {127,0,0,1}, @@ -401,7 +404,8 @@ tunnel_in_non_erlclient_erlserver(Config) -> {_Pid, Host, Port} = ssh_test_lib:daemon([{tcpip_tunnel_in, true}, {system_dir, SystemDir}, ?ALIVE, - {failfun, fun ssh_test_lib:failfun/2}]), + {failfun, fun ssh_test_lib:failfun/2}, + {shell, {shell, start, []}}]), {ToSock, _ToHost, ToPort} = tunneling_listner(), ListenHost = {127,0,0,1}, From 3dbfc213e54313bf5e8770d6c31a354b0921ff9d Mon Sep 17 00:00:00 2001 From: Jakub Witczak Date: Thu, 2 Apr 2026 18:30:39 +0200 Subject: [PATCH 2/2] ssh: Disable SFTP subsystem by default The subsystems daemon option now defaults to an empty list instead of enabling the SFTP subsystem via ssh_sftpd:subsystem_spec([]). This extends the "secure by default" principle to also cover SFTP, complementing the shell and exec default changes. Previously, all authenticated SSH users had access to the entire file system visible to the Erlang VM process through the default SFTP subsystem, with no path restrictions. Changes: - Change subsystems default from [ssh_sftpd:subsystem_spec([])] to [] - Update subsystem_spec doc string in ssh.hrl - Remove dead code in ssh_connection:check_subsystem/2 - Update test suites to explicitly enable SFTP where needed - Update documentation (hardening.md, introduction.md, using_ssh.md, ssh_app.md, terminology.md) Applications requiring SFTP must now explicitly enable it: ssh:daemon(Port, [{subsystems, [ssh_sftpd:subsystem_spec([])]} | Options]) --- lib/ssh/doc/guides/hardening.md | 6 ++- lib/ssh/doc/guides/introduction.md | 11 ++--- lib/ssh/doc/guides/terminology.md | 5 ++- lib/ssh/doc/guides/using_ssh.md | 3 +- lib/ssh/doc/ssh_app.md | 5 ++- lib/ssh/src/ssh.hrl | 11 +++-- lib/ssh/src/ssh_connection.erl | 23 +--------- lib/ssh/src/ssh_options.erl | 2 +- lib/ssh/test/ssh_algorithms_SUITE.erl | 4 +- lib/ssh/test/ssh_basic_SUITE.erl | 1 + lib/ssh/test/ssh_compat_SUITE.erl | 1 + lib/ssh/test/ssh_connection_SUITE.erl | 50 +++++++++++++++++++++- lib/ssh/test/ssh_dbg_SUITE.erl | 5 ++- lib/ssh/test/ssh_options_SUITE.erl | 3 +- lib/ssh/test/ssh_renegotiate_SUITE.erl | 27 ++++++++---- lib/ssh/test/ssh_sftp_SUITE.erl | 3 +- lib/ssh/test/ssh_sftpd_erlclient_SUITE.erl | 2 +- lib/ssh/test/ssh_sup_SUITE.erl | 8 +++- 18 files changed, 115 insertions(+), 55 deletions(-) diff --git a/lib/ssh/doc/guides/hardening.md b/lib/ssh/doc/guides/hardening.md index ab1472179ed1..06782a9acc41 100644 --- a/lib/ssh/doc/guides/hardening.md +++ b/lib/ssh/doc/guides/hardening.md @@ -122,7 +122,7 @@ excessive memory consumption. ### SFTP Server Resource Limits -When running an SFTP server via `ssh_sftpd:subsystem_spec/1`, additional +When enabling the SFTP subsystem via `ssh_sftpd:subsystem_spec/1`, additional resource limits can be configured to protect against resource exhaustion attacks: #### max_handles @@ -357,6 +357,10 @@ The negotiation (session setup time) time can be limited with the _parameter_ ## SFTP Security +Note that the SFTP server runs with the file access rights of the OS +process running the Erlang emulator, regardless of the authenticated +SSH user. See the [Terminology](terminology.md) section for details. + ### Root Directory Isolation The `root` option (see `m:ssh_sftpd`) restricts SFTP users to a diff --git a/lib/ssh/doc/guides/introduction.md b/lib/ssh/doc/guides/introduction.md index 4cc5e28abfaa..8a97ae18d549 100644 --- a/lib/ssh/doc/guides/introduction.md +++ b/lib/ssh/doc/guides/introduction.md @@ -34,8 +34,9 @@ The `ssh` application is an implementation of the SSH Transport, Connection and Authentication Layer Protocols in Erlang. It provides the following: - API functions to write customized SSH clients and servers applications -- The Erlang shell available over SSH -- An SFTP client (`m:ssh_sftp`) and server (`m:ssh_sftpd`) +- The Erlang shell available over SSH, enabled via the `shell` daemon option +- An SFTP client (`m:ssh_sftp`) and server (`m:ssh_sftpd`), enabled via the + `subsystems` daemon option ## Prerequisites @@ -114,9 +115,9 @@ the application logic. Channels come in the following three flavors: - _Subsystem_ \- Named services that can be run as part of an SSH server, such - as SFTP [(ssh_sftpd)](`m:ssh_sftpd`), that is built into the SSH daemon - (server) by default, but it can be disabled. The Erlang `ssh` daemon can be - configured to run any Erlang- implemented SSH subsystem. + as SFTP [(ssh_sftpd)](`m:ssh_sftpd`). No subsystems are enabled by default. + The Erlang `ssh` daemon can be configured to run any Erlang-implemented SSH + subsystem. - _Shell_ \- Interactive shell. By default the Erlang daemon does not expose the Erlang shell. It can be enabled with option `{shell, {shell, start, []}}` The shell can be customized by providing your own read-eval-print loop. diff --git a/lib/ssh/doc/guides/terminology.md b/lib/ssh/doc/guides/terminology.md index c7e39f0bba16..3e87f22b1da6 100644 --- a/lib/ssh/doc/guides/terminology.md +++ b/lib/ssh/doc/guides/terminology.md @@ -157,8 +157,9 @@ the server's emulator. The rights in that shell is independent of the just authenticated user. In case of an sftp request, an sftp server is started with the rights of the -user of the Erlang emulator's OS process. So with sftp the authenticated user -does not influence the rights. +user of the Erlang emulator's OS process, provided the SFTP subsystem is +enabled. So with sftp, the file access rights are those of the OS process +running the Erlang emulator, regardless of the authenticated SSH user. So after an authentication, the user name is not used anymore and has no influence. diff --git a/lib/ssh/doc/guides/using_ssh.md b/lib/ssh/doc/guides/using_ssh.md index 983ab7af2c2f..b0b5729e7ad5 100644 --- a/lib/ssh/doc/guides/using_ssh.md +++ b/lib/ssh/doc/guides/using_ssh.md @@ -433,7 +433,8 @@ and `ClientAddress`). See the ## SFTP Server -Start the Erlang `ssh` daemon with the SFTP subsystem: +The SFTP subsystem is not enabled by default. To start an SSH daemon with +SFTP, configure the `subsystems` option explicitly: ```erlang 1> ssh:start(). diff --git a/lib/ssh/doc/ssh_app.md b/lib/ssh/doc/ssh_app.md index be8b1ba2d999..700ea8220dbd 100644 --- a/lib/ssh/doc/ssh_app.md +++ b/lib/ssh/doc/ssh_app.md @@ -28,8 +28,9 @@ SSH File Transfer Protocol (SFTP) client and server. The `ssh` application is an implementation of the SSH protocol in Erlang. `ssh` offers API functions to write customized SSH clients and servers as well as -making the Erlang shell available over SSH. An SFTP client, `ssh_sftp`, and -server, `ssh_sftpd`, are also included. +making the Erlang shell available over SSH. An SFTP client (`ssh_sftp`) and +server (`ssh_sftpd`) are also included. The SFTP server can be enabled via +the `subsystems` daemon option. ## Dependencies diff --git a/lib/ssh/src/ssh.hrl b/lib/ssh/src/ssh.hrl index 604f250e2fbf..378759203b26 100644 --- a/lib/ssh/src/ssh.hrl +++ b/lib/ssh/src/ssh.hrl @@ -208,10 +208,13 @@ The `channel_callback` is the module that implements the `m:ssh_server_channel` [Creating a Subsystem](using_ssh.md#usersguide_creating_a_subsystem) in the User's Guide for more information and an example. -If the subsystems option is not present, the value of -`ssh_sftpd:subsystem_spec([])` is used. This enables the sftp subsystem by -default. The option can be set to the empty list if you do not want the daemon -to run any subsystems. +If the subsystems option is not present, the default is an empty list +and no subsystems are enabled. + +To enable the SFTP subsystem: +``` +ssh:daemon(Port, [{subsystems, [ssh_sftpd:subsystem_spec([])]} | Options]) +``` """. -doc(#{group => <<"Daemon Options">>}). -type subsystem_spec() :: {Name::string(), mod_args()} . diff --git a/lib/ssh/src/ssh_connection.erl b/lib/ssh/src/ssh_connection.erl index d16a1e2fa153..8c8c6dd18dab 100644 --- a/lib/ssh/src/ssh_connection.erl +++ b/lib/ssh/src/ssh_connection.erl @@ -1505,7 +1505,8 @@ start_subsystem(BinName, #connection{options = Options, connection_supervisor = ConnectionSup}, #channel{local_id = ChannelId}, _ReplyMsg) -> Name = binary_to_list(BinName), - case check_subsystem(Name, Options) of + Subsystems = ?GET_OPT(subsystems, Options), + case proplists:get_value(Name, Subsystems, {none, []}) of {Callback, Opts} when is_atom(Callback), Callback =/= none -> %% Exec is not used by subsystems; undefined is filtered %% out by channel_cb_init_args/1 so it won't be appended @@ -1518,26 +1519,6 @@ start_subsystem(BinName, #connection{options = Options, {error, legacy_option_not_supported} end. - -%%% Helpers for starting cli/subsystems -check_subsystem("sftp"= SsName, Options) -> - case ?GET_OPT(subsystems, Options) of - no_subsys -> % FIXME: Can 'no_subsys' ever be matched? - {SsName, {Cb, Opts}} = ssh_sftpd:subsystem_spec([]), - {Cb, Opts}; - SubSystems -> - proplists:get_value(SsName, SubSystems, {none, []}) - end; - -check_subsystem(SsName, Options) -> - Subsystems = ?GET_OPT(subsystems, Options), - case proplists:get_value(SsName, Subsystems, {none, []}) of - Fun when is_function(Fun) -> - {Fun, []}; - {_, _} = Value -> - Value - end. - %%%---------------------------------------------------------------- %%% %%% Send-window handling diff --git a/lib/ssh/src/ssh_options.erl b/lib/ssh/src/ssh_options.erl index b100874a5ac2..77e0245be049 100644 --- a/lib/ssh/src/ssh_options.erl +++ b/lib/ssh/src/ssh_options.erl @@ -405,7 +405,7 @@ default(server) -> (default(common)) #{ subsystems => - #{default => [ssh_sftpd:subsystem_spec([])], + #{default => [], chk => fun(L) -> is_list(L) andalso lists:all(fun(SubSystem = {Name,{CB,Args}}) -> diff --git a/lib/ssh/test/ssh_algorithms_SUITE.erl b/lib/ssh/test/ssh_algorithms_SUITE.erl index a47f46ad147e..24527a99e420 100644 --- a/lib/ssh/test/ssh_algorithms_SUITE.erl +++ b/lib/ssh/test/ssh_algorithms_SUITE.erl @@ -231,7 +231,9 @@ init_per_group(Tag, Algs, Alg, PA, Config) -> end; _ -> - start_std_daemon([{exec, erlang_eval}] ++ [PrefAlgs], + start_std_daemon([{exec, erlang_eval}, + {subsystems, [ssh_sftpd:subsystem_spec([])]} + | [PrefAlgs]], [{pref_algs,PrefAlgs}, {tag_alg,{Tag,[Alg]}} | Config]) diff --git a/lib/ssh/test/ssh_basic_SUITE.erl b/lib/ssh/test/ssh_basic_SUITE.erl index 5ae5523e6ce6..84319dcbeb3c 100644 --- a/lib/ssh/test/ssh_basic_SUITE.erl +++ b/lib/ssh/test/ssh_basic_SUITE.erl @@ -459,6 +459,7 @@ idle_time_common(DaemonExtraOpts, ClientExtraOpts, Config) -> UserDir = proplists:get_value(priv_dir, Config), {Pid, Host, Port} = ssh_test_lib:daemon([{exec, erlang_eval}, + {subsystems, [ssh_sftpd:subsystem_spec([])]}, {system_dir, SystemDir}, {user_dir, UserDir}, {failfun, fun ssh_test_lib:failfun/2} diff --git a/lib/ssh/test/ssh_compat_SUITE.erl b/lib/ssh/test/ssh_compat_SUITE.erl index e81f4ec40aac..ee288561d2bd 100644 --- a/lib/ssh/test/ssh_compat_SUITE.erl +++ b/lib/ssh/test/ssh_compat_SUITE.erl @@ -349,6 +349,7 @@ renegotiation_otp_is_server(Config) -> [{system_dir, setup_local_hostdir(Config)}, {user_dir, UserDir}, {user_passwords, [{?USER,?PASSWD}]}, + {subsystems, [ssh_sftpd:subsystem_spec([])]}, {failfun, fun ssh_test_lib:failfun/2}, {modify_algorithms, [{append, [{public_key,PublicKeyAlgs}]}]}, {connectfun, diff --git a/lib/ssh/test/ssh_connection_SUITE.erl b/lib/ssh/test/ssh_connection_SUITE.erl index 2d2569bd3f9b..aff752613a80 100644 --- a/lib/ssh/test/ssh_connection_SUITE.erl +++ b/lib/ssh/test/ssh_connection_SUITE.erl @@ -112,6 +112,7 @@ trap_exit_connect/1, trap_exit_daemon/1, handler_down_before_open/1, + replace_options_enable_services/1, ssh_exec_echo/2 % called as an MFA ]). @@ -187,7 +188,8 @@ all() -> no_sensitive_leak, start_subsystem_on_closed_channel, max_channels_option, - handler_down_before_open + handler_down_before_open, + replace_options_enable_services ]. groups() -> [{openssh, [], payload() ++ ptty() ++ sock()}]. @@ -2092,6 +2094,52 @@ test_exec_is_disabled(ConnectionRef) -> ct:fail("Exec Timeout") end. +%%-------------------------------------------------------------------- +replace_options_enable_services(Config) when is_list(Config) -> + PrivDir = proplists:get_value(priv_dir, Config), + UserDir = filename:join(PrivDir, nopubkey), + file:make_dir(UserDir), + SysDir = proplists:get_value(data_dir, Config), + {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir}, + {user_dir, UserDir}, + {password, "morot"}]), + ConnOpts = [{silently_accept_hosts, true}, + {user, "foo"}, + {password, "morot"}, + {user_dir, UserDir}], + + %% Verify all services disabled before replace + C1 = ssh_test_lib:connect(Host, Port, ConnOpts), + ?CT_LOG("Checking shell is disabled before replace", []), + test_shell_is_disabled(C1), + ?CT_LOG("Checking exec is disabled before replace", []), + test_exec_is_disabled(C1), + ?CT_LOG("Checking SFTP is unavailable before replace", []), + {error, _} = ssh_sftp:start_channel(C1), + ?CT_LOG("All services confirmed disabled", []), + ssh:close(C1), + + %% Enable shell, exec and SFTP + ?CT_LOG("Replacing options: enabling shell, exec and SFTP", []), + {ok, Pid} = ssh:daemon_replace_options(Pid, + [{shell, {shell, start, []}}, + {exec, erlang_eval}, + {subsystems, [ssh_sftpd:subsystem_spec([])]}]), + + %% Verify all services work after replace + C2 = ssh_test_lib:connect(Host, Port, ConnOpts), + ?CT_LOG("Checking shell is enabled after replace", []), + test_shell_is_enabled(C2), + ?CT_LOG("Checking exec is enabled after replace", []), + test_exec_is_enabled(C2), + ?CT_LOG("Checking SFTP is available after replace", []), + {ok, SftpPid} = ssh_sftp:start_channel(C2), + ssh_sftp:stop_channel(SftpPid), + ?CT_LOG("All services confirmed enabled after replace", []), + + ssh:close(C2), + ssh:stop_daemon(Pid). + %%-------------------------------------------------------------------- test_shell_is_enabled(ConnectionRef) -> test_shell_is_enabled(ConnectionRef, <<"Eshell V">>). diff --git a/lib/ssh/test/ssh_dbg_SUITE.erl b/lib/ssh/test/ssh_dbg_SUITE.erl index 6f9b3f98a2ff..00d2cc4571ad 100644 --- a/lib/ssh/test/ssh_dbg_SUITE.erl +++ b/lib/ssh/test/ssh_dbg_SUITE.erl @@ -481,8 +481,9 @@ all_dbg(Config) -> {_, Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir}, - {user_dir, UserDir}, - {user_passwords, [{?USR,?PWD}]} + {user_dir, UserDir}, + {user_passwords, [{?USR,?PWD}]}, + {subsystems, [ssh_sftpd:subsystem_spec([])]} ]), {ok, ChPid, _C} = diff --git a/lib/ssh/test/ssh_options_SUITE.erl b/lib/ssh/test/ssh_options_SUITE.erl index 3d6525535196..664e36dc593a 100644 --- a/lib/ssh/test/ssh_options_SUITE.erl +++ b/lib/ssh/test/ssh_options_SUITE.erl @@ -1551,7 +1551,8 @@ max_sessions(Config, ParallelLogin, Connect0) when is_function(Connect0,2) -> {user_dir, UserDir}, {user_passwords, [{"carni", "meat"}]}, {parallel_login, ParallelLogin}, - {max_sessions, MaxSessions} + {max_sessions, MaxSessions}, + {subsystems, [ssh_sftpd:subsystem_spec([])]} ]), ct:log("~p Listen ~p:~p for max ~p sessions",[Pid,Host,Port,MaxSessions]), try [Connect(Host,Port) || _ <- lists:seq(1,MaxSessions)] diff --git a/lib/ssh/test/ssh_renegotiate_SUITE.erl b/lib/ssh/test/ssh_renegotiate_SUITE.erl index 2b7494f1756b..358574dbf05a 100644 --- a/lib/ssh/test/ssh_renegotiate_SUITE.erl +++ b/lib/ssh/test/ssh_renegotiate_SUITE.erl @@ -144,7 +144,8 @@ rekey4(Config) -> rekey_chk(Config, 0, {infinity,infinity}). rekey_chk(Config, RLdaemon, RLclient) -> {Pid, Host, Port} = ssh_test_lib:std_daemon(Config, - [{rekey_limit, RLdaemon}, ?ALIVE]), + [{subsystems, [ssh_sftpd:subsystem_spec([])]}, + {rekey_limit, RLdaemon}, ?ALIVE]), ConnectionRef = ssh_test_lib:std_connect(Config, Host, Port, [{rekey_limit, RLclient}, ?ALIVE]), @@ -172,7 +173,8 @@ rekey_limit_client(Config) -> Algs = proplists:get_value(preferred_algorithms, Config), {Pid, Host, Port} = ssh_test_lib:std_daemon(Config, - [{max_random_length_padding,0}, + [{subsystems, [ssh_sftpd:subsystem_spec([])]}, + {max_random_length_padding,0}, ?ALIVE, {preferred_algorithms,Algs}]), @@ -226,7 +228,8 @@ rekey_limit_daemon(Config) -> Algs = proplists:get_value(preferred_algorithms, Config), {Pid, Host, Port} = - ssh_test_lib:std_daemon(Config, [{rekey_limit, Limit}, + ssh_test_lib:std_daemon(Config, [{subsystems, [ssh_sftpd:subsystem_spec([])]}, + {rekey_limit, Limit}, {max_random_length_padding,0}, ?ALIVE, {preferred_algorithms,Algs}]), @@ -279,7 +282,8 @@ norekey_limit_client(Config) -> Algs = proplists:get_value(preferred_algorithms, Config), {Pid, Host, Port} = - ssh_test_lib:std_daemon(Config,[{max_random_length_padding,0}, + ssh_test_lib:std_daemon(Config,[{subsystems, [ssh_sftpd:subsystem_spec([])]}, + {max_random_length_padding,0}, ?ALIVE, {preferred_algorithms,Algs}]), @@ -311,7 +315,8 @@ norekey_limit_daemon(Config) -> Algs = proplists:get_value(preferred_algorithms, Config), {Pid, Host, Port} = - ssh_test_lib:std_daemon(Config,[{rekey_limit, Limit}, + ssh_test_lib:std_daemon(Config,[{subsystems, [ssh_sftpd:subsystem_spec([])]}, + {rekey_limit, Limit}, {max_random_length_padding,0}, ?ALIVE, {preferred_algorithms,Algs}]), @@ -342,7 +347,8 @@ rekey_time_limit_client(Config) -> GB = 1024*1000*1000, Algs = proplists:get_value(preferred_algorithms, Config), {Pid, Host, Port} = - ssh_test_lib:std_daemon(Config,[{max_random_length_padding,0}, + ssh_test_lib:std_daemon(Config,[{subsystems, [ssh_sftpd:subsystem_spec([])]}, + {max_random_length_padding,0}, ?ALIVE, {preferred_algorithms,Algs}]), ConnectionRef = @@ -358,7 +364,8 @@ rekey_time_limit_daemon(Config) -> GB = 1024*1000*1000, Algs = proplists:get_value(preferred_algorithms, Config), {Pid, Host, Port} = - ssh_test_lib:std_daemon(Config,[{rekey_limit, {Minutes, GB}}, + ssh_test_lib:std_daemon(Config,[{subsystems, [ssh_sftpd:subsystem_spec([])]}, + {rekey_limit, {Minutes, GB}}, {max_random_length_padding,0}, ?ALIVE, {preferred_algorithms,Algs}]), @@ -397,7 +404,8 @@ renegotiate1(Config) -> Algs = proplists:get_value(preferred_algorithms, Config), {Pid, Host, DPort} = - ssh_test_lib:std_daemon(Config,[{max_random_length_padding,0}, + ssh_test_lib:std_daemon(Config,[{subsystems, [ssh_sftpd:subsystem_spec([])]}, + {max_random_length_padding,0}, ?ALIVE, {preferred_algorithms,Algs}]), @@ -440,7 +448,8 @@ renegotiate2(Config) -> Algs = proplists:get_value(preferred_algorithms, Config), {Pid, Host, DPort} = ssh_test_lib:std_daemon(Config, - [{max_random_length_padding,0}, + [{subsystems, [ssh_sftpd:subsystem_spec([])]}, + {max_random_length_padding,0}, ?ALIVE, {preferred_algorithms,Algs}]), diff --git a/lib/ssh/test/ssh_sftp_SUITE.erl b/lib/ssh/test/ssh_sftp_SUITE.erl index 6ee2bc9bfa1c..f01ef59242f9 100644 --- a/lib/ssh/test/ssh_sftp_SUITE.erl +++ b/lib/ssh/test/ssh_sftp_SUITE.erl @@ -230,7 +230,8 @@ init_per_group(erlang_server, Config) -> ssh_test_lib:daemon([{system_dir, SysDir}, {user_dir, PrivDir}, {user_passwords, - [{User, Passwd}]}]), + [{User, Passwd}]}, + {subsystems, [ssh_sftpd:subsystem_spec([])]}]), [{peer, {fmt_host(HostX),PortX}}, {group, erlang_server}, {sftpd, Sftpd} | Config]; init_per_group(openssh_server, Config) -> diff --git a/lib/ssh/test/ssh_sftpd_erlclient_SUITE.erl b/lib/ssh/test/ssh_sftpd_erlclient_SUITE.erl index 604789d30748..60730a0ad28f 100644 --- a/lib/ssh/test/ssh_sftpd_erlclient_SUITE.erl +++ b/lib/ssh/test/ssh_sftpd_erlclient_SUITE.erl @@ -124,7 +124,7 @@ init_per_testcase(TestCase, Config) -> Spec = ssh_sftpd:subsystem_spec([{sftpd_vsn, 6}]), [{subsystems, [Spec]}]; _ -> - [] + [{subsystems, [ssh_sftpd:subsystem_spec([])]}] end, {Sftpd, Host, Port} = ssh_test_lib:daemon([{preferred_algorithms, diff --git a/lib/ssh/test/ssh_sup_SUITE.erl b/lib/ssh/test/ssh_sup_SUITE.erl index 4032e7fcdb09..26dc7b723c69 100644 --- a/lib/ssh/test/ssh_sup_SUITE.erl +++ b/lib/ssh/test/ssh_sup_SUITE.erl @@ -97,7 +97,8 @@ init_per_testcase(sshc_subtree, Config) -> {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir}, {failfun, fun ssh_test_lib:failfun/2}, {user_passwords, - [{?USER, ?PASSWD}]}]), + [{?USER, ?PASSWD}]}, + {subsystems, [ssh_sftpd:subsystem_spec([])]}]), [{server, {Pid, Host, Port}} | Config]; init_per_testcase(Case, Config) -> end_per_testcase(Case, Config), @@ -160,7 +161,8 @@ sshd_subtree(Config) when is_list(Config) -> {Daemon, HostIP, Port} = ssh_test_lib:daemon([{system_dir, SystemDir}, {failfun, fun ssh_test_lib:failfun/2}, {user_passwords, - [{?USER, ?PASSWD}]}]), + [{?USER, ?PASSWD}]}, + {subsystems, [ssh_sftpd:subsystem_spec([])]}]), ct:log("Expect HostIP=~p, Port=~p, Daemon=~p",[HostIP,Port,Daemon]), ?wait_match([?SYSTEM_SUP(Daemon, #address{address=ListenIP, port=Port, @@ -182,6 +184,7 @@ sshd_subtree_profile(Config) when is_list(Config) -> {failfun, fun ssh_test_lib:failfun/2}, {user_passwords, [{?USER, ?PASSWD}]}, + {subsystems, [ssh_sftpd:subsystem_spec([])]}, {profile, Profile}]), ct:log("Expect HostIP=~p, Port=~p, Profile=~p, Daemon=~p",[HostIP,Port,Profile,Daemon]), ?wait_match([?SYSTEM_SUP(Daemon, #address{address=ListenIP, @@ -293,6 +296,7 @@ shell_channel_tree(Config) -> {Daemon, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir}, {user_dir, UserDir}, {password, "morot"}, + {subsystems, [ssh_sftpd:subsystem_spec([])]}, {shell, fun(_User) -> spawn(TimeoutShell) end