|
| 1 | +From 043ee3c943e2977c1acdd740ad13992fd60b6bf0 Mon Sep 17 00:00:00 2001 |
| 2 | +From: Jakub Witczak < [email protected]> |
| 3 | +Date: Fri, 11 Jul 2025 13:59:41 +0200 |
| 4 | +Subject: [PATCH] ssh: ssh_sftpd verify path size for client data |
| 5 | + |
| 6 | +- reject max_path exceeding the 4096 limit or according to other option value |
| 7 | + |
| 8 | +Modified to apply to Azure Linux |
| 9 | +Modified by: Akhila Guruju < [email protected]> |
| 10 | +Date: Fri, 3 Oct 2025 11:07:55 +0000 |
| 11 | + |
| 12 | +Upstream Patch Reference: https://github.com/erlang/otp/commit/043ee3c943e2977c1acdd740ad13992fd60b6bf0.patch |
| 13 | +--- |
| 14 | + lib/ssh/doc/src/ssh_sftpd.xml | 8 ++++ |
| 15 | + lib/ssh/src/ssh_sftpd.erl | 28 ++++++++++++ |
| 16 | + lib/ssh/test/ssh_sftpd_SUITE.erl | 78 ++++++++++++++++++++------------ |
| 17 | + 3 files changed, 85 insertions(+), 29 deletions(-) |
| 18 | + |
| 19 | +diff --git a/lib/ssh/doc/src/ssh_sftpd.xml b/lib/ssh/doc/src/ssh_sftpd.xml |
| 20 | +index 03e8dad..cbe015f 100644 |
| 21 | +--- a/lib/ssh/doc/src/ssh_sftpd.xml |
| 22 | ++++ b/lib/ssh/doc/src/ssh_sftpd.xml |
| 23 | +@@ -65,6 +65,14 @@ |
| 24 | + If supplied, the number of filenames returned to the SFTP client per <c>READDIR</c> |
| 25 | + request is limited to at most the given value.</p> |
| 26 | + </item> |
| 27 | ++ <tag><c>max_path</c></tag> |
| 28 | ++ <item> |
| 29 | ++ <p>The default value is <c>4096</c>. Positive integer |
| 30 | ++ value represents the maximum path length which cannot be |
| 31 | ++ exceeded in data provided by the SFTP client. (Note: |
| 32 | ++ limitations might be also enforced by underlying operating |
| 33 | ++ system)</p> |
| 34 | ++ </item> |
| 35 | + <tag><c>max_handles</c></tag> |
| 36 | + <item> |
| 37 | + <p>The default value is <c>1000</c>. Positive integer value represents the maximum number of file handles allowed for a connection.</p> |
| 38 | +diff --git a/lib/ssh/src/ssh_sftpd.erl b/lib/ssh/src/ssh_sftpd.erl |
| 39 | +index 0c64178..d02ece3 100644 |
| 40 | +--- a/lib/ssh/src/ssh_sftpd.erl |
| 41 | ++++ b/lib/ssh/src/ssh_sftpd.erl |
| 42 | +@@ -52,6 +52,7 @@ |
| 43 | + file_handler, % atom() - callback module |
| 44 | + file_state, % state for the file callback module |
| 45 | + max_files, % integer >= 0 max no files sent during READDIR |
| 46 | ++ max_path, % integer > 0 - max length of path |
| 47 | + max_handles, % integer > 0 - max number of file handles |
| 48 | + options, % from the subsystem declaration |
| 49 | + handles % list of open handles |
| 50 | +@@ -66,6 +67,7 @@ |
| 51 | + Options :: [ {cwd, string()} | |
| 52 | + {file_handler, CbMod | {CbMod, FileState}} | |
| 53 | + {max_files, integer()} | |
| 54 | ++ {max_path, integer()} | |
| 55 | + {max_handles, integer()} | |
| 56 | + {root, string()} | |
| 57 | + {sftpd_vsn, integer()} |
| 58 | +@@ -117,11 +119,13 @@ init(Options) -> |
| 59 | + {Root0, State0} |
| 60 | + end, |
| 61 | + MaxLength = proplists:get_value(max_files, Options, 0), |
| 62 | ++ MaxPath = proplists:get_value(max_path, Options, 4096), |
| 63 | + MaxHandles = proplists:get_value(max_handles, Options, 1000), |
| 64 | + Vsn = proplists:get_value(sftpd_vsn, Options, 5), |
| 65 | + {ok, State#state{cwd = CWD, |
| 66 | + root = Root, |
| 67 | + max_files = MaxLength, |
| 68 | ++ max_path = MaxPath, |
| 69 | + max_handles = MaxHandles, |
| 70 | + options = Options, |
| 71 | + handles = [], pending = <<>>, |
| 72 | +@@ -239,6 +243,30 @@ handle_op(Request, ReqId, <<?UINT32(HLen), _/binary>>, State = #state{xf = XF}) |
| 73 | + HLen > 256 -> |
| 74 | + ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_INVALID_HANDLE, "Invalid handle"), |
| 75 | + State; |
| 76 | ++handle_op(Request, ReqId, <<?UINT32(PLen), _/binary>>, |
| 77 | ++ State = #state{max_path = MaxPath, xf = XF}) |
| 78 | ++ when (Request == ?SSH_FXP_LSTAT orelse |
| 79 | ++ Request == ?SSH_FXP_MKDIR orelse |
| 80 | ++ Request == ?SSH_FXP_OPEN orelse |
| 81 | ++ Request == ?SSH_FXP_OPENDIR orelse |
| 82 | ++ Request == ?SSH_FXP_READLINK orelse |
| 83 | ++ Request == ?SSH_FXP_REALPATH orelse |
| 84 | ++ Request == ?SSH_FXP_REMOVE orelse |
| 85 | ++ Request == ?SSH_FXP_RMDIR orelse |
| 86 | ++ Request == ?SSH_FXP_SETSTAT orelse |
| 87 | ++ Request == ?SSH_FXP_STAT), |
| 88 | ++ PLen > MaxPath -> |
| 89 | ++ ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_NO_SUCH_PATH, |
| 90 | ++ "No such path"), |
| 91 | ++ State; |
| 92 | ++handle_op(Request, ReqId, <<?UINT32(PLen), _:PLen/binary, ?UINT32(PLen2), _/binary>>, |
| 93 | ++ State = #state{max_path = MaxPath, xf = XF}) |
| 94 | ++ when (Request == ?SSH_FXP_RENAME orelse |
| 95 | ++ Request == ?SSH_FXP_SYMLINK), |
| 96 | ++ (PLen > MaxPath orelse PLen2 > MaxPath) -> |
| 97 | ++ ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_NO_SUCH_PATH, |
| 98 | ++ "No such path"), |
| 99 | ++ State; |
| 100 | + handle_op(?SSH_FXP_INIT, Version, B, State) when is_binary(B) -> |
| 101 | + XF = State#state.xf, |
| 102 | + Vsn = lists:min([XF#ssh_xfer.vsn, Version]), |
| 103 | +diff --git a/lib/ssh/test/ssh_sftpd_SUITE.erl b/lib/ssh/test/ssh_sftpd_SUITE.erl |
| 104 | +index 9da2e41..01321ed 100644 |
| 105 | +--- a/lib/ssh/test/ssh_sftpd_SUITE.erl |
| 106 | ++++ b/lib/ssh/test/ssh_sftpd_SUITE.erl |
| 107 | +@@ -43,6 +43,7 @@ |
| 108 | + open_file_dir_v6/1, |
| 109 | + read_dir/1, |
| 110 | + read_file/1, |
| 111 | ++ max_path/1, |
| 112 | + real_path/1, |
| 113 | + relative_path/1, |
| 114 | + relpath/1, |
| 115 | +@@ -71,6 +72,7 @@ |
| 116 | + -define(REG_ATTERS, <<0,0,0,0,1>>). |
| 117 | + -define(UNIX_EPOCH, 62167219200). |
| 118 | + -define(MAX_HANDLES, 10). |
| 119 | ++-define(MAX_PATH, 200). |
| 120 | + -define(is_set(F, Bits), ((F) band (Bits)) == (F)). |
| 121 | + |
| 122 | + %%-------------------------------------------------------------------- |
| 123 | +@@ -84,6 +86,7 @@ all() -> |
| 124 | + [open_close_file, |
| 125 | + open_close_dir, |
| 126 | + read_file, |
| 127 | ++ max_path, |
| 128 | + read_dir, |
| 129 | + write_file, |
| 130 | + rename_file, |
| 131 | +@@ -177,7 +180,9 @@ init_per_testcase(TestCase, Config) -> |
| 132 | + {sftpd_vsn, 6}])], |
| 133 | + ssh:daemon(0, [{subsystems, SubSystems}|Options]); |
| 134 | + _ -> |
| 135 | +- SubSystems = [ssh_sftpd:subsystem_spec([{max_handles, ?MAX_HANDLES}])], |
| 136 | ++ SubSystems = [ssh_sftpd:subsystem_spec( |
| 137 | ++ [{max_handles, ?MAX_HANDLES}, |
| 138 | ++ {max_path, ?MAX_PATH}])], |
| 139 | + ssh:daemon(0, [{subsystems, SubSystems}|Options]) |
| 140 | + end, |
| 141 | + |
| 142 | +@@ -333,6 +338,23 @@ read_file(Config) when is_list(Config) -> |
| 143 | + ct:log("Message: ~s", [Msg]), |
| 144 | + ok. |
| 145 | + |
| 146 | ++%%-------------------------------------------------------------------- |
| 147 | ++max_path(Config) when is_list(Config) -> |
| 148 | ++ PrivDir = proplists:get_value(priv_dir, Config), |
| 149 | ++ FileName = filename:join(PrivDir, "test.txt"), |
| 150 | ++ {Cm, Channel} = proplists:get_value(sftp, Config), |
| 151 | ++ %% verify max_path limit |
| 152 | ++ LongFileName = |
| 153 | ++ filename:join(PrivDir, |
| 154 | ++ "t" ++ lists:flatten(lists:duplicate(?MAX_PATH, "e")) ++ "st.txt"), |
| 155 | ++ {ok, _} = file:copy(FileName, LongFileName), |
| 156 | ++ ReqId1 = req_id(), |
| 157 | ++ {ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId1), ?UINT32(?SSH_FX_NO_SUCH_PATH), |
| 158 | ++ _/binary>>, _} = |
| 159 | ++ open_file(LongFileName, Cm, Channel, ReqId1, |
| 160 | ++ ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES, |
| 161 | ++ ?SSH_FXF_OPEN_EXISTING). |
| 162 | ++ |
| 163 | + read_dir(Config) when is_list(Config) -> |
| 164 | + PrivDir = proplists:get_value(priv_dir, Config), |
| 165 | + {Cm, Channel} = proplists:get_value(sftp, Config), |
| 166 | +@@ -396,35 +418,33 @@ rename_file(Config) when is_list(Config) -> |
| 167 | + PrivDir = proplists:get_value(priv_dir, Config), |
| 168 | + FileName = filename:join(PrivDir, "test.txt"), |
| 169 | + NewFileName = filename:join(PrivDir, "test1.txt"), |
| 170 | +- ReqId = 0, |
| 171 | ++ LongFileName = |
| 172 | ++ filename:join(PrivDir, |
| 173 | ++ "t" ++ lists:flatten(lists:duplicate(?MAX_PATH, "e")) ++ "st.txt"), |
| 174 | + {Cm, Channel} = proplists:get_value(sftp, Config), |
| 175 | +- |
| 176 | +- {ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId), |
| 177 | +- ?UINT32(?SSH_FX_OK), _/binary>>, _} = |
| 178 | +- rename(FileName, NewFileName, Cm, Channel, ReqId, 6, 0), |
| 179 | +- |
| 180 | +- NewReqId = ReqId + 1, |
| 181 | +- |
| 182 | +- {ok, <<?SSH_FXP_STATUS, ?UINT32(NewReqId), |
| 183 | +- ?UINT32(?SSH_FX_OK), _/binary>>, _} = |
| 184 | +- rename(NewFileName, FileName, Cm, Channel, NewReqId, 6, |
| 185 | +- ?SSH_FXP_RENAME_OVERWRITE), |
| 186 | +- |
| 187 | +- NewReqId1 = NewReqId + 1, |
| 188 | +- file:copy(FileName, NewFileName), |
| 189 | +- |
| 190 | +- %% No overwrite |
| 191 | +- {ok, <<?SSH_FXP_STATUS, ?UINT32(NewReqId1), |
| 192 | +- ?UINT32(?SSH_FX_FILE_ALREADY_EXISTS), _/binary>>, _} = |
| 193 | +- rename(FileName, NewFileName, Cm, Channel, NewReqId1, 6, |
| 194 | +- ?SSH_FXP_RENAME_NATIVE), |
| 195 | +- |
| 196 | +- NewReqId2 = NewReqId1 + 1, |
| 197 | +- |
| 198 | +- {ok, <<?SSH_FXP_STATUS, ?UINT32(NewReqId2), |
| 199 | +- ?UINT32(?SSH_FX_OP_UNSUPPORTED), _/binary>>, _} = |
| 200 | +- rename(FileName, NewFileName, Cm, Channel, NewReqId2, 6, |
| 201 | +- ?SSH_FXP_RENAME_ATOMIC). |
| 202 | ++ Version = 6, |
| 203 | ++ [begin |
| 204 | ++ case Action of |
| 205 | ++ {Code, AFile, BFile, Flags} -> |
| 206 | ++ ReqId = req_id(), |
| 207 | ++ ct:log("ReqId = ~p,~nCode = ~p,~nAFile = ~p,~nBFile = ~p,~nFlags = ~p", |
| 208 | ++ [ReqId, Code, AFile, BFile, Flags]), |
| 209 | ++ {ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId), ?UINT32(Code), _/binary>>, _} = |
| 210 | ++ rename(AFile, BFile, Cm, Channel, ReqId, Version, Flags); |
| 211 | ++ {file_copy, AFile, BFile} -> |
| 212 | ++ {ok, _} = file:copy(AFile, BFile) |
| 213 | ++ end |
| 214 | ++ end || |
| 215 | ++ Action <- |
| 216 | ++ [{?SSH_FX_OK, FileName, NewFileName, 0}, |
| 217 | ++ {?SSH_FX_OK, NewFileName, FileName, ?SSH_FXP_RENAME_OVERWRITE}, |
| 218 | ++ {file_copy, FileName, NewFileName}, |
| 219 | ++ %% no overwrite |
| 220 | ++ {?SSH_FX_FILE_ALREADY_EXISTS, FileName, NewFileName, ?SSH_FXP_RENAME_NATIVE}, |
| 221 | ++ {?SSH_FX_OP_UNSUPPORTED, FileName, NewFileName, ?SSH_FXP_RENAME_ATOMIC}, |
| 222 | ++ %% max_path |
| 223 | ++ {?SSH_FX_NO_SUCH_PATH, FileName, LongFileName, 0}]], |
| 224 | ++ ok. |
| 225 | + |
| 226 | + %%-------------------------------------------------------------------- |
| 227 | + mk_rm_dir(Config) when is_list(Config) -> |
| 228 | +-- |
| 229 | +2.43.0 |
| 230 | + |
0 commit comments