Skip to content

Commit 746290c

Browse files
committed
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.
1 parent 92c5353 commit 746290c

16 files changed

Lines changed: 197 additions & 115 deletions

lib/kernel/test/shell_test_lib.erl

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,9 +182,10 @@ setup_tty(Config) ->
182182
PrivDir = filename:join(proplists:get_value(priv_dir, Config), "nopubkey"),
183183
file:make_dir(PrivDir),
184184
SysDir = proplists:get_value(data_dir, Config),
185-
{ok, _Sshd} = ssh:daemon(8989, [{system_dir, SysDir},
186-
{user_dir, PrivDir},
187-
{password, "bar"}])
185+
{ok, _Sshd} = ssh:daemon(8989, [{shell, {shell, start, []}},
186+
{system_dir, SysDir},
187+
{user_dir, PrivDir},
188+
{password, "bar"}])
188189
end),
189190
os:cmd(os:find_executable("tmux") ++ " new-window -n " ++ ClientName ++ " -d -- "++
190191
"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null localhost -p 8989 -l foo"),

lib/ssh/doc/guides/hardening.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -303,10 +303,9 @@ _exec_ server-side service takes a string provided by the client, evaluates it
303303
and returns the result. The _shell_ function enables the client to open a shell
304304
in the shell host.
305305

306-
Those service could - and should - be disabled when they are not needed. The
307-
options [exec](`t:ssh:exec_daemon_option/0`) and
308-
[shell](`t:ssh:shell_daemon_option/0`) are enabled per default but could be set
309-
to `disabled` if not needed. The same options could also install handlers for
306+
The options [exec](`t:ssh:exec_daemon_option/0`) and
307+
[shell](`t:ssh:shell_daemon_option/0`) are disabled per default.
308+
The same options could also install handlers for
310309
the string(s) passed from the client to the server.
311310

312311
### The id string

lib/ssh/doc/guides/introduction.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,12 +117,13 @@ Channels come in the following three flavors:
117117
as SFTP [(ssh_sftpd)](`m:ssh_sftpd`), that is built into the SSH daemon
118118
(server) by default, but it can be disabled. The Erlang `ssh` daemon can be
119119
configured to run any Erlang- implemented SSH subsystem.
120-
- _Shell_ \- Interactive shell. By default the Erlang daemon runs the Erlang
121-
shell. The shell can be customized by providing your own read-eval-print loop.
120+
- _Shell_ \- Interactive shell. By default the Erlang daemon does not expose the Erlang
121+
shell. It can be enabled with option `{shell, {shell, start, []}}`
122+
The shell can be customized by providing your own read-eval-print loop.
122123
You can also provide your own Command-Line Interface (CLI) implementation, but
123124
that is much more work.
124-
- _Exec_ \- One-time remote execution of commands. See function
125-
`ssh_connection:exec/4` for more information.
125+
- _Exec_ \- By default one-time remote execution of commands is disabled.
126+
See function `ssh_connection:exec/4` for more information.
126127

127128
## Where to Find More Information
128129

lib/ssh/doc/guides/using_ssh.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ _Step 3._ Start the Erlang `ssh` daemon:
8383
1> ssh:start().
8484
ok
8585
2> {ok, Sshd} = ssh:daemon(8989, [{system_dir, "/tmp/ssh_daemon"},
86-
{user_dir, "/tmp/otptest_user/.ssh"}]).
86+
{user_dir, "/tmp/otptest_user/.ssh"},
87+
{shell, {shell, start, []}}]).
8788
{ok,<0.54.0>}
8889
3>
8990
```

lib/ssh/src/ssh.hrl

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,6 @@
3636

3737
-define(DEFAULT_TRANSPORT, {tcp, gen_tcp, tcp_closed} ).
3838

39-
-define(DEFAULT_SHELL, {shell, start, []} ).
40-
4139
-define(DEFAULT_TIMEOUT, 5000).
4240

4341
-define(MAX_RND_PADDING_LEN, 15).
@@ -842,6 +840,14 @@ risk.
842840
-type shell_daemon_option() :: {shell, shell_spec()} .
843841
-doc(#{group => <<"Daemon Options">>}).
844842
-type shell_spec() :: mod_fun_args() | shell_fun() | disabled .
843+
-doc """
844+
The default is `disabled`.
845+
846+
To enable the Erlang shell (the behavior from OTP versions prior to OTP @OTP-19969@):
847+
```
848+
ssh:daemon(Port, [{shell, {shell, start, []}} | Options])
849+
```
850+
""".
845851
-doc(#{group => <<"Daemon Options">>,
846852
equiv => 'shell_fun/2'/0}).
847853
-type shell_fun() :: 'shell_fun/1'() | 'shell_fun/2'() .
@@ -850,7 +856,7 @@ risk.
850856
-type 'shell_fun/1'() :: fun((User::string()) -> pid()) .
851857
-doc """
852858
Defines the read-eval-print loop used in a daemon when a shell is requested by
853-
the client. The default is to use the Erlang shell: `{shell, start, []}`
859+
the client.
854860

855861
See the option [`exec-option`](`t:exec_daemon_option/0`) for a description of
856862
how the daemon executes shell-requests and exec-requests depending on the shell-
@@ -862,7 +868,23 @@ and exec-options.
862868
-doc(#{group => <<"Daemon Options">>}).
863869
-type exec_daemon_option() :: {exec, exec_spec()} .
864870
-doc(#{group => <<"Daemon Options">>}).
865-
-type exec_spec() :: {direct, exec_fun()} | disabled | deprecated_exec_opt().
871+
-type exec_spec() :: {direct, exec_fun()} | disabled | deprecated_exec_opt() | erlang_eval.
872+
-doc """
873+
The default is `disabled`.
874+
875+
Value `erlang_eval` enables evaluation of Erlang terms via exec requests.
876+
This works when the shell option is either `disabled` (no shell) or
877+
`{shell, start, []}` (Erlang shell). It does not work with custom shells.
878+
879+
To restore the behavior from OTP versions prior to OTP @OTP-19969@, configure:
880+
```
881+
ssh:daemon(Port, [{shell, {shell, start, []}},
882+
{exec, erlang_eval}
883+
| Options])
884+
```
885+
886+
For new code, consider using `{direct, Fun}` for more controlled exec handling.
887+
""".
866888
-doc(#{group => <<"Daemon Options">>}).
867889
-type exec_fun() :: 'exec_fun/1'() | 'exec_fun/2'() | 'exec_fun/3'().
868890
-doc(#{group => <<"Daemon Options">>}).

lib/ssh/src/ssh_cli.erl

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -41,19 +41,20 @@
4141

4242
%% state
4343
-record(state, {
44-
cm,
45-
channel,
46-
pty,
47-
encoding,
48-
deduced_encoding, % OpenSSH sometimes lies about its encodeing. This variable
49-
% is for the process of guessing the peer encoding, taylord
50-
% after the behaviour of openssh. If it says latin1 it is so.
51-
% It there arrives characters encoded in latin1 it is so. Otherwise
52-
% assume utf8 until otherwise is proved.
53-
group,
54-
shell,
55-
exec,
56-
tty
44+
cm,
45+
channel,
46+
pty,
47+
encoding,
48+
%% OpenSSH sometimes lies about its encodeing. This variable
49+
%% is for the process of guessing the peer encoding, taylord
50+
%% after the behaviour of openssh. If it says latin1 it is so.
51+
%% It there arrives characters encoded in latin1 it is so. Otherwise
52+
%% assume utf8 until otherwise is proved.
53+
deduced_encoding,
54+
group,
55+
shell,
56+
exec = erlang_eval,
57+
tty
5758

5859
}).
5960

@@ -212,16 +213,16 @@ handle_ssh_msg({ssh_cm, ConnectionHandler, {exec, ChannelId, WantReply, Cmd0}},
212213
%% The standard I/O is directed from/to the channel ChannelId.
213214
exec_direct(ConnectionHandler, ChannelId, Cmd, F, WantReply, S1);
214215

215-
undefined when S0#state.shell == ?DEFAULT_SHELL ;
216-
S0#state.shell == disabled ->
217-
%% Exec called and the shell is the default shell (= Erlang shell).
218-
%% To be exact, eval the term as an Erlang term (but not using the
219-
%% ?DEFAULT_SHELL directly). This disables banner, prompts and such.
216+
erlang_eval when S0#state.shell == disabled;
217+
S0#state.shell == {shell, start, []} ->
218+
%% Exec called and the shell is the Erlang shell or disabled.
219+
%% To be exact, eval the term as an Erlang term
220+
%% This disables banner, prompts and such.
220221
%% The standard I/O is directed from/to the channel ChannelId.
221222
exec_in_erlang_default_shell(ConnectionHandler, ChannelId, Cmd, WantReply, S1);
222223

223-
undefined ->
224-
%% Exec called, but the a shell other than the default shell is defined.
224+
erlang_eval ->
225+
%% Exec called, but the shell is custom (not Erlang shell).
225226
%% No new exec shell is defined, so don't execute!
226227
%% We don't know if it is intended to use the new shell or not.
227228
{"Prohibited.", ?EXEC_ERROR_STATUS, 1};

lib/ssh/src/ssh_client_channel.erl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,11 @@ init([Options]) ->
420420

421421
channel_cb_init_args(Options) ->
422422
case proplists:get_value(exec, Options) of
423+
erlang_eval ->
424+
%% erlang_eval is a mode flag for ssh_cli, not a cb init arg
425+
proplists:get_value(init_args, Options);
423426
undefined ->
427+
%% exec key absent from Options (client-side ssh:shell/1)
424428
proplists:get_value(init_args, Options);
425429
Exec ->
426430
proplists:get_value(init_args, Options) ++ [Exec]

lib/ssh/src/ssh_connection.erl

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ these messages are handled by
5050
-include("ssh.hrl").
5151
-include("ssh_connect.hrl").
5252
-include("ssh_transport.hrl").
53-
5453
%% API
5554
-export([session_channel/2, session_channel/4,
5655
exec/4, shell/2, subsystem/4, send/3, send/4, send/5,
@@ -1497,7 +1496,8 @@ start_cli(#connection{options = Options,
14971496
no_cli ->
14981497
{error, cli_disabled};
14991498
{CbModule, Args} ->
1500-
ssh_connection_sup:start_channel(server, ConnectionSup, self(), CbModule, ChannelId, Args, Exec, Options)
1499+
ssh_connection_sup:start_channel(server, ConnectionSup, self(),
1500+
CbModule, ChannelId, Args, Exec, Options)
15011501
end.
15021502

15031503

@@ -1507,7 +1507,11 @@ start_subsystem(BinName, #connection{options = Options,
15071507
Name = binary_to_list(BinName),
15081508
case check_subsystem(Name, Options) of
15091509
{Callback, Opts} when is_atom(Callback), Callback =/= none ->
1510-
ssh_connection_sup:start_channel(server, ConnectionSup, self(), Callback, ChannelId, Opts, undefined, Options);
1510+
%% Exec is not used by subsystems; undefined is filtered
1511+
%% out by channel_cb_init_args/1 so it won't be appended
1512+
%% to the callback's init args.
1513+
ssh_connection_sup:start_channel(server, ConnectionSup, self(),
1514+
Callback, ChannelId, Opts, undefined, Options);
15111515
{none, _} ->
15121516
{error, bad_subsystem};
15131517
{_, _} ->

lib/ssh/src/ssh_options.erl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,7 @@ default(server) ->
421421
},
422422

423423
shell =>
424-
#{default => ?DEFAULT_SHELL,
424+
#{default => disabled,
425425
chk => fun({M,F,A}) -> is_atom(M) andalso is_atom(F) andalso is_list(A);
426426
(disabled) -> true;
427427
(V) -> check_function1(V) orelse
@@ -431,9 +431,10 @@ default(server) ->
431431
},
432432

433433
exec =>
434-
#{default => undefined,
434+
#{default => disabled,
435435
chk => fun({direct, V}) -> check_function1(V) orelse check_function2(V) orelse check_function3(V);
436436
(disabled) -> true;
437+
(erlang_eval) -> true; % Enable Erlang term evaluation
437438
%% Compatibility (undocumented):
438439
({M,F,A}) -> is_atom(M) andalso is_atom(F) andalso is_list(A);
439440
(V) -> check_function1(V) orelse check_function2(V) orelse check_function3(V)

lib/ssh/src/ssh_sftp.erl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ start_channel(Cm, UserOptions0) when is_pid(Cm) ->
172172
PacketSize = proplists:get_value(packet_size, ChanOpts, ?XFER_PACKET_SIZE),
173173
case ssh_connection:session_channel(Cm, WindowSize, PacketSize, Timeout) of
174174
{ok, ChannelId} ->
175+
%% Exec is not used by SFTP channels; undefined is filtered
176+
%% out by channel_cb_init_args/1.
175177
case ssh_connection_handler:start_channel(Cm, ?MODULE, ChannelId,
176178
[Cm,ChannelId,SftpOpts], undefined) of
177179
{ok, Pid} ->

0 commit comments

Comments
 (0)