Skip to content

Commit 043ee3c

Browse files
committed
ssh: ssh_sftpd verify path size for client data
- reject max_path exceeding the 4096 limit or according to other option value
1 parent c1ce39f commit 043ee3c

File tree

3 files changed

+97
-33
lines changed

3 files changed

+97
-33
lines changed

lib/ssh/doc/src/ssh_sftpd.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@
6565
If supplied, the number of filenames returned to the SFTP client per <c>READDIR</c>
6666
request is limited to at most the given value.</p>
6767
</item>
68+
<tag><c>max_path</c></tag>
69+
<item>
70+
<p>The default value is <c>4096</c>. Positive integer
71+
value represents the maximum path length which cannot be
72+
exceeded in data provided by the SFTP client. (Note:
73+
limitations might be also enforced by underlying operating
74+
system)</p>
75+
</item>
6876
<tag><c>root</c></tag>
6977
<item>
7078
<p>Sets the SFTP root directory. Then the user cannot see any files

lib/ssh/src/ssh_sftpd.erl

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
file_handler, % atom() - callback module
5353
file_state, % state for the file callback module
5454
max_files, % integer >= 0 max no files sent during READDIR
55+
max_path, % integer > 0 - max length of path
5556
options, % from the subsystem declaration
5657
handles % list of open handles
5758
%% handle is either {<int>, directory, {Path, unread|eof}} or
@@ -65,6 +66,7 @@
6566
Options :: [ {cwd, string()} |
6667
{file_handler, CbMod | {CbMod, FileState}} |
6768
{max_files, integer()} |
69+
{max_path, integer()} |
6870
{root, string()} |
6971
{sftpd_vsn, integer()}
7072
],
@@ -115,8 +117,12 @@ init(Options) ->
115117
{Root0, State0}
116118
end,
117119
MaxLength = proplists:get_value(max_files, Options, 0),
120+
MaxPath = proplists:get_value(max_path, Options, 4096),
118121
Vsn = proplists:get_value(sftpd_vsn, Options, 5),
119-
{ok, State#state{cwd = CWD, root = Root, max_files = MaxLength,
122+
{ok, State#state{cwd = CWD,
123+
root = Root,
124+
max_files = MaxLength,
125+
max_path = MaxPath,
120126
options = Options,
121127
handles = [], pending = <<>>,
122128
xf = #ssh_xfer{vsn = Vsn, ext = []}}}.
@@ -222,6 +228,30 @@ handle_data(Type, ChannelId, Data0, State = #state{pending = Pending}) ->
222228
handle_data(Type, ChannelId, Data, State#state{pending = <<>>})
223229
end.
224230

231+
handle_op(Request, ReqId, <<?UINT32(PLen), _/binary>>,
232+
State = #state{max_path = MaxPath, xf = XF})
233+
when (Request == ?SSH_FXP_LSTAT orelse
234+
Request == ?SSH_FXP_MKDIR orelse
235+
Request == ?SSH_FXP_OPEN orelse
236+
Request == ?SSH_FXP_OPENDIR orelse
237+
Request == ?SSH_FXP_READLINK orelse
238+
Request == ?SSH_FXP_REALPATH orelse
239+
Request == ?SSH_FXP_REMOVE orelse
240+
Request == ?SSH_FXP_RMDIR orelse
241+
Request == ?SSH_FXP_SETSTAT orelse
242+
Request == ?SSH_FXP_STAT),
243+
PLen > MaxPath ->
244+
ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_NO_SUCH_PATH,
245+
"No such path"),
246+
State;
247+
handle_op(Request, ReqId, <<?UINT32(PLen), _:PLen/binary, ?UINT32(PLen2), _/binary>>,
248+
State = #state{max_path = MaxPath, xf = XF})
249+
when (Request == ?SSH_FXP_RENAME orelse
250+
Request == ?SSH_FXP_SYMLINK),
251+
(PLen > MaxPath orelse PLen2 > MaxPath) ->
252+
ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_NO_SUCH_PATH,
253+
"No such path"),
254+
State;
225255
handle_op(?SSH_FXP_INIT, Version, B, State) when is_binary(B) ->
226256
XF = State#state.xf,
227257
Vsn = lists:min([XF#ssh_xfer.vsn, Version]),

lib/ssh/test/ssh_sftpd_SUITE.erl

Lines changed: 58 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
open_file_dir_v6/1,
4444
read_dir/1,
4545
read_file/1,
46+
max_path/1,
4647
real_path/1,
4748
relative_path/1,
4849
relpath/1,
@@ -72,9 +73,8 @@
7273
-define(SSH_TIMEOUT, 10000).
7374
-define(REG_ATTERS, <<0,0,0,0,1>>).
7475
-define(UNIX_EPOCH, 62167219200).
75-
76-
-define(is_set(F, Bits),
77-
((F) band (Bits)) == (F)).
76+
-define(MAX_PATH, 200).
77+
-define(is_set(F, Bits), ((F) band (Bits)) == (F)).
7878

7979
%%--------------------------------------------------------------------
8080
%% Common Test interface functions -----------------------------------
@@ -87,6 +87,7 @@ all() ->
8787
[open_close_file,
8888
open_close_dir,
8989
read_file,
90+
max_path,
9091
read_dir,
9192
write_file,
9293
rename_file,
@@ -181,7 +182,8 @@ init_per_testcase(TestCase, Config) ->
181182
{sftpd_vsn, 6}])],
182183
ssh:daemon(0, [{subsystems, SubSystems}|Options]);
183184
_ ->
184-
SubSystems = [ssh_sftpd:subsystem_spec([])],
185+
SubSystems = [ssh_sftpd:subsystem_spec(
186+
[{max_path, ?MAX_PATH}])],
185187
ssh:daemon(0, [{subsystems, SubSystems}|Options])
186188
end,
187189

@@ -334,6 +336,23 @@ read_file(Config) when is_list(Config) ->
334336

335337
{ok, Data} = file:read_file(FileName).
336338

339+
%%--------------------------------------------------------------------
340+
max_path(Config) when is_list(Config) ->
341+
PrivDir = proplists:get_value(priv_dir, Config),
342+
FileName = filename:join(PrivDir, "test.txt"),
343+
{Cm, Channel} = proplists:get_value(sftp, Config),
344+
%% verify max_path limit
345+
LongFileName =
346+
filename:join(PrivDir,
347+
"t" ++ lists:flatten(lists:duplicate(?MAX_PATH, "e")) ++ "st.txt"),
348+
{ok, _} = file:copy(FileName, LongFileName),
349+
ReqId1 = req_id(),
350+
{ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId1), ?UINT32(?SSH_FX_NO_SUCH_PATH),
351+
_/binary>>, _} =
352+
open_file(LongFileName, Cm, Channel, ReqId1,
353+
?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES,
354+
?SSH_FXF_OPEN_EXISTING).
355+
337356
%%--------------------------------------------------------------------
338357
read_dir(Config) when is_list(Config) ->
339358
PrivDir = proplists:get_value(priv_dir, Config),
@@ -389,35 +408,33 @@ rename_file(Config) when is_list(Config) ->
389408
PrivDir = proplists:get_value(priv_dir, Config),
390409
FileName = filename:join(PrivDir, "test.txt"),
391410
NewFileName = filename:join(PrivDir, "test1.txt"),
392-
ReqId = 0,
411+
LongFileName =
412+
filename:join(PrivDir,
413+
"t" ++ lists:flatten(lists:duplicate(?MAX_PATH, "e")) ++ "st.txt"),
393414
{Cm, Channel} = proplists:get_value(sftp, Config),
394-
395-
{ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId),
396-
?UINT32(?SSH_FX_OK), _/binary>>, _} =
397-
rename(FileName, NewFileName, Cm, Channel, ReqId, 6, 0),
398-
399-
NewReqId = ReqId + 1,
400-
401-
{ok, <<?SSH_FXP_STATUS, ?UINT32(NewReqId),
402-
?UINT32(?SSH_FX_OK), _/binary>>, _} =
403-
rename(NewFileName, FileName, Cm, Channel, NewReqId, 6,
404-
?SSH_FXP_RENAME_OVERWRITE),
405-
406-
NewReqId1 = NewReqId + 1,
407-
file:copy(FileName, NewFileName),
408-
409-
%% No overwrite
410-
{ok, <<?SSH_FXP_STATUS, ?UINT32(NewReqId1),
411-
?UINT32(?SSH_FX_FILE_ALREADY_EXISTS), _/binary>>, _} =
412-
rename(FileName, NewFileName, Cm, Channel, NewReqId1, 6,
413-
?SSH_FXP_RENAME_NATIVE),
414-
415-
NewReqId2 = NewReqId1 + 1,
416-
417-
{ok, <<?SSH_FXP_STATUS, ?UINT32(NewReqId2),
418-
?UINT32(?SSH_FX_OP_UNSUPPORTED), _/binary>>, _} =
419-
rename(FileName, NewFileName, Cm, Channel, NewReqId2, 6,
420-
?SSH_FXP_RENAME_ATOMIC).
415+
Version = 6,
416+
[begin
417+
case Action of
418+
{Code, AFile, BFile, Flags} ->
419+
ReqId = req_id(),
420+
ct:log("ReqId = ~p,~nCode = ~p,~nAFile = ~p,~nBFile = ~p,~nFlags = ~p",
421+
[ReqId, Code, AFile, BFile, Flags]),
422+
{ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId), ?UINT32(Code), _/binary>>, _} =
423+
rename(AFile, BFile, Cm, Channel, ReqId, Version, Flags);
424+
{file_copy, AFile, BFile} ->
425+
{ok, _} = file:copy(AFile, BFile)
426+
end
427+
end ||
428+
Action <-
429+
[{?SSH_FX_OK, FileName, NewFileName, 0},
430+
{?SSH_FX_OK, NewFileName, FileName, ?SSH_FXP_RENAME_OVERWRITE},
431+
{file_copy, FileName, NewFileName},
432+
%% no overwrite
433+
{?SSH_FX_FILE_ALREADY_EXISTS, FileName, NewFileName, ?SSH_FXP_RENAME_NATIVE},
434+
{?SSH_FX_OP_UNSUPPORTED, FileName, NewFileName, ?SSH_FXP_RENAME_ATOMIC},
435+
%% max_path
436+
{?SSH_FX_NO_SUCH_PATH, FileName, LongFileName, 0}]],
437+
ok.
421438

422439
%%--------------------------------------------------------------------
423440
mk_rm_dir(Config) when is_list(Config) ->
@@ -1087,3 +1104,12 @@ encode_file_type(Type) ->
10871104

10881105
not_default_permissions() ->
10891106
8#600. %% User read-write-only
1107+
1108+
req_id() ->
1109+
ReqId =
1110+
case get(req_id) of
1111+
undefined -> 0;
1112+
I -> I
1113+
end,
1114+
put(req_id, ReqId + 1),
1115+
ReqId.

0 commit comments

Comments
 (0)