Package: release.debian.org Severity: normal Tags: bookworm X-Debbugs-Cc: [email protected], Sergei Golovan <[email protected]>, Bastien Roucaries <[email protected]> Control: affects -1 + src:erlang User: [email protected] Usertags: pu
[ Reason ] There have been several CVEs published for the erlang programming language that have been flagged as no DSA affecting the ssh server implementation. [ Impact ] Mostly denial of service attacks. [ Tests ] Manually tested. [ Risks ] Low risk, given that those implementations are niche and the patches mostly add safe guards. [ Checklist ] [X] *all* changes are documented in the d/changelog [X] I reviewed all changes and I approve them [X] attach debdiff against the package in (old)stable [X] the issue is verified as fixed in unstable [ Other info ] @Sergei as with #1127606 (trixie) please write if you disagree.
diff --git a/debian/changelog b/debian/changelog index 6738cb3c3a..b6b3272c74 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,20 @@ +erlang (1:25.2.3+dfsg-1+deb12u4) bookworm; urgency=medium + + * Non-maintainer upload. + * Fix CVE-2025-48038: allocation of resources without limits or throttling + vulnerability in the ssh_sftp module allows excessive allocation, + resource leak exposure (closes: #1115093). + * Fix CVE-2025-48039: allocation of resources without limits or throttling + vulnerability in the ssh_sftp module allows excessive allocation, + resource leak exposure (closes: #1115092). + * Fix CVE-2025-48040: uncontrolled resource consumption vulnerability in + the ssh_sftp module allows excessive allocation, flooding (closes: 1115091). + * Fix CVE-2025-48041: allocation of resources without limits or throttling + vulnerability in the ssh_sftp module allows excessive allocation, + flooding (closes: #1115090). + + -- Jochen Sprickerhof <[email protected]> Thu, 15 Jan 2026 10:56:30 +0100 + erlang (1:25.2.3+dfsg-1+deb12u3) bookworm-proposed-updates; urgency=medium * Fix FTBFS with newer xsltproc. diff --git a/debian/gbp.conf b/debian/gbp.conf new file mode 100644 index 0000000000..cec628c744 --- /dev/null +++ b/debian/gbp.conf @@ -0,0 +1,2 @@ +[DEFAULT] +pristine-tar = True diff --git a/debian/patches/CVE-2025-48038.patch b/debian/patches/CVE-2025-48038.patch new file mode 100644 index 0000000000..800160e857 --- /dev/null +++ b/debian/patches/CVE-2025-48038.patch @@ -0,0 +1,34 @@ +From: Jakub Witczak <[email protected]> +Date: Wed, 27 Aug 2025 17:49:08 +0200 +Subject: ssh: verify file handle size limit for client data + +- reject handles exceeding 256 bytes (as specified for SFTP) + +Origin: https://github.com/erlang/otp/commit/f09e0201ff701993dc24a08f15e524daf72db42f +Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2025-48038 +--- + lib/ssh/src/ssh_sftpd.erl | 11 +++++++++++ + 1 file changed, 11 insertions(+) + +diff --git a/lib/ssh/src/ssh_sftpd.erl b/lib/ssh/src/ssh_sftpd.erl +index 6bcad0d..cd24c3e 100644 +--- a/lib/ssh/src/ssh_sftpd.erl ++++ b/lib/ssh/src/ssh_sftpd.erl +@@ -222,6 +222,17 @@ handle_data(Type, ChannelId, Data0, State = #state{pending = Pending}) -> + handle_data(Type, ChannelId, Data, State#state{pending = <<>>}) + end. + ++%% From draft-ietf-secsh-filexfer-02 "The file handle strings MUST NOT be longer than 256 bytes." ++handle_op(Request, ReqId, <<?UINT32(HLen), _/binary>>, State = #state{xf = XF}) ++ when (Request == ?SSH_FXP_CLOSE orelse ++ Request == ?SSH_FXP_FSETSTAT orelse ++ Request == ?SSH_FXP_FSTAT orelse ++ Request == ?SSH_FXP_READ orelse ++ Request == ?SSH_FXP_READDIR orelse ++ Request == ?SSH_FXP_WRITE), ++ HLen > 256 -> ++ ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_INVALID_HANDLE, "Invalid handle"), ++ State; + handle_op(?SSH_FXP_INIT, Version, B, State) when is_binary(B) -> + XF = State#state.xf, + Vsn = lists:min([XF#ssh_xfer.vsn, Version]), diff --git a/debian/patches/CVE-2025-48039.patch b/debian/patches/CVE-2025-48039.patch new file mode 100644 index 0000000000..a683c023c0 --- /dev/null +++ b/debian/patches/CVE-2025-48039.patch @@ -0,0 +1,239 @@ +From: Jakub Witczak <[email protected]> +Date: Fri, 11 Jul 2025 13:59:41 +0200 +Subject: ssh: ssh_sftpd verify path size for client data + +- reject max_path exceeding the 4096 limit or according to other option value + +Origin: https://github.com/erlang/otp/commit/043ee3c943e2977c1acdd740ad13992fd60b6bf0 +Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2025-48039 +--- + lib/ssh/doc/src/ssh_sftpd.xml | 8 ++++ + lib/ssh/src/ssh_sftpd.erl | 32 +++++++++++++- + lib/ssh/test/ssh_sftpd_SUITE.erl | 90 ++++++++++++++++++++++++++-------------- + 3 files changed, 97 insertions(+), 33 deletions(-) + +diff --git a/lib/ssh/doc/src/ssh_sftpd.xml b/lib/ssh/doc/src/ssh_sftpd.xml +index 49a23f4..efabf3f 100644 +--- a/lib/ssh/doc/src/ssh_sftpd.xml ++++ b/lib/ssh/doc/src/ssh_sftpd.xml +@@ -65,6 +65,14 @@ + If supplied, the number of filenames returned to the SFTP client per <c>READDIR</c> + request is limited to at most the given value.</p> + </item> ++ <tag><c>max_path</c></tag> ++ <item> ++ <p>The default value is <c>4096</c>. Positive integer ++ value represents the maximum path length which cannot be ++ exceeded in data provided by the SFTP client. (Note: ++ limitations might be also enforced by underlying operating ++ system)</p> ++ </item> + <tag><c>root</c></tag> + <item> + <p>Sets the SFTP root directory. Then the user cannot see any files +diff --git a/lib/ssh/src/ssh_sftpd.erl b/lib/ssh/src/ssh_sftpd.erl +index cd24c3e..5632848 100644 +--- a/lib/ssh/src/ssh_sftpd.erl ++++ b/lib/ssh/src/ssh_sftpd.erl +@@ -52,6 +52,7 @@ + file_handler, % atom() - callback module + file_state, % state for the file callback module + max_files, % integer >= 0 max no files sent during READDIR ++ max_path, % integer > 0 - max length of path + options, % from the subsystem declaration + handles % list of open handles + %% handle is either {<int>, directory, {Path, unread|eof}} or +@@ -65,6 +66,7 @@ + Options :: [ {cwd, string()} | + {file_handler, CbMod | {CbMod, FileState}} | + {max_files, integer()} | ++ {max_path, integer()} | + {root, string()} | + {sftpd_vsn, integer()} + ], +@@ -115,8 +117,12 @@ init(Options) -> + {Root0, State0} + end, + MaxLength = proplists:get_value(max_files, Options, 0), ++ MaxPath = proplists:get_value(max_path, Options, 4096), + Vsn = proplists:get_value(sftpd_vsn, Options, 5), +- {ok, State#state{cwd = CWD, root = Root, max_files = MaxLength, ++ {ok, State#state{cwd = CWD, ++ root = Root, ++ max_files = MaxLength, ++ max_path = MaxPath, + options = Options, + handles = [], pending = <<>>, + xf = #ssh_xfer{vsn = Vsn, ext = []}}}. +@@ -233,6 +239,30 @@ handle_op(Request, ReqId, <<?UINT32(HLen), _/binary>>, State = #state{xf = XF}) + HLen > 256 -> + ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_INVALID_HANDLE, "Invalid handle"), + State; ++handle_op(Request, ReqId, <<?UINT32(PLen), _/binary>>, ++ State = #state{max_path = MaxPath, xf = XF}) ++ when (Request == ?SSH_FXP_LSTAT orelse ++ Request == ?SSH_FXP_MKDIR orelse ++ Request == ?SSH_FXP_OPEN orelse ++ Request == ?SSH_FXP_OPENDIR orelse ++ Request == ?SSH_FXP_READLINK orelse ++ Request == ?SSH_FXP_REALPATH orelse ++ Request == ?SSH_FXP_REMOVE orelse ++ Request == ?SSH_FXP_RMDIR orelse ++ Request == ?SSH_FXP_SETSTAT orelse ++ Request == ?SSH_FXP_STAT), ++ PLen > MaxPath -> ++ ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_NO_SUCH_PATH, ++ "No such path"), ++ State; ++handle_op(Request, ReqId, <<?UINT32(PLen), _:PLen/binary, ?UINT32(PLen2), _/binary>>, ++ State = #state{max_path = MaxPath, xf = XF}) ++ when (Request == ?SSH_FXP_RENAME orelse ++ Request == ?SSH_FXP_SYMLINK), ++ (PLen > MaxPath orelse PLen2 > MaxPath) -> ++ ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_NO_SUCH_PATH, ++ "No such path"), ++ State; + handle_op(?SSH_FXP_INIT, Version, B, State) when is_binary(B) -> + XF = State#state.xf, + Vsn = lists:min([XF#ssh_xfer.vsn, Version]), +diff --git a/lib/ssh/test/ssh_sftpd_SUITE.erl b/lib/ssh/test/ssh_sftpd_SUITE.erl +index 42677b7..f04cde3 100644 +--- a/lib/ssh/test/ssh_sftpd_SUITE.erl ++++ b/lib/ssh/test/ssh_sftpd_SUITE.erl +@@ -43,6 +43,7 @@ + open_file_dir_v6/1, + read_dir/1, + read_file/1, ++ max_path/1, + real_path/1, + relative_path/1, + relpath/1, +@@ -71,9 +72,8 @@ + -define(SSH_TIMEOUT, 10000). + -define(REG_ATTERS, <<0,0,0,0,1>>). + -define(UNIX_EPOCH, 62167219200). +- +--define(is_set(F, Bits), +- ((F) band (Bits)) == (F)). ++-define(MAX_PATH, 200). ++-define(is_set(F, Bits), ((F) band (Bits)) == (F)). + + %%-------------------------------------------------------------------- + %% Common Test interface functions ----------------------------------- +@@ -86,6 +86,7 @@ all() -> + [open_close_file, + open_close_dir, + read_file, ++ max_path, + read_dir, + write_file, + rename_file, +@@ -180,7 +181,8 @@ init_per_testcase(TestCase, Config) -> + {sftpd_vsn, 6}])], + ssh:daemon(0, [{subsystems, SubSystems}|Options]); + _ -> +- SubSystems = [ssh_sftpd:subsystem_spec([])], ++ SubSystems = [ssh_sftpd:subsystem_spec( ++ [{max_path, ?MAX_PATH}])], + ssh:daemon(0, [{subsystems, SubSystems}|Options]) + end, + +@@ -333,6 +335,23 @@ read_file(Config) when is_list(Config) -> + + {ok, Data} = file:read_file(FileName). + ++%%-------------------------------------------------------------------- ++max_path(Config) when is_list(Config) -> ++ PrivDir = proplists:get_value(priv_dir, Config), ++ FileName = filename:join(PrivDir, "test.txt"), ++ {Cm, Channel} = proplists:get_value(sftp, Config), ++ %% verify max_path limit ++ LongFileName = ++ filename:join(PrivDir, ++ "t" ++ lists:flatten(lists:duplicate(?MAX_PATH, "e")) ++ "st.txt"), ++ {ok, _} = file:copy(FileName, LongFileName), ++ ReqId1 = req_id(), ++ {ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId1), ?UINT32(?SSH_FX_NO_SUCH_PATH), ++ _/binary>>, _} = ++ open_file(LongFileName, Cm, Channel, ReqId1, ++ ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES, ++ ?SSH_FXF_OPEN_EXISTING). ++ + %%-------------------------------------------------------------------- + read_dir(Config) when is_list(Config) -> + PrivDir = proplists:get_value(priv_dir, Config), +@@ -388,35 +407,33 @@ rename_file(Config) when is_list(Config) -> + PrivDir = proplists:get_value(priv_dir, Config), + FileName = filename:join(PrivDir, "test.txt"), + NewFileName = filename:join(PrivDir, "test1.txt"), +- ReqId = 0, ++ LongFileName = ++ filename:join(PrivDir, ++ "t" ++ lists:flatten(lists:duplicate(?MAX_PATH, "e")) ++ "st.txt"), + {Cm, Channel} = proplists:get_value(sftp, Config), +- +- {ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId), +- ?UINT32(?SSH_FX_OK), _/binary>>, _} = +- rename(FileName, NewFileName, Cm, Channel, ReqId, 6, 0), +- +- NewReqId = ReqId + 1, +- +- {ok, <<?SSH_FXP_STATUS, ?UINT32(NewReqId), +- ?UINT32(?SSH_FX_OK), _/binary>>, _} = +- rename(NewFileName, FileName, Cm, Channel, NewReqId, 6, +- ?SSH_FXP_RENAME_OVERWRITE), +- +- NewReqId1 = NewReqId + 1, +- file:copy(FileName, NewFileName), +- +- %% No overwrite +- {ok, <<?SSH_FXP_STATUS, ?UINT32(NewReqId1), +- ?UINT32(?SSH_FX_FILE_ALREADY_EXISTS), _/binary>>, _} = +- rename(FileName, NewFileName, Cm, Channel, NewReqId1, 6, +- ?SSH_FXP_RENAME_NATIVE), +- +- NewReqId2 = NewReqId1 + 1, +- +- {ok, <<?SSH_FXP_STATUS, ?UINT32(NewReqId2), +- ?UINT32(?SSH_FX_OP_UNSUPPORTED), _/binary>>, _} = +- rename(FileName, NewFileName, Cm, Channel, NewReqId2, 6, +- ?SSH_FXP_RENAME_ATOMIC). ++ Version = 6, ++ [begin ++ case Action of ++ {Code, AFile, BFile, Flags} -> ++ ReqId = req_id(), ++ ct:log("ReqId = ~p,~nCode = ~p,~nAFile = ~p,~nBFile = ~p,~nFlags = ~p", ++ [ReqId, Code, AFile, BFile, Flags]), ++ {ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId), ?UINT32(Code), _/binary>>, _} = ++ rename(AFile, BFile, Cm, Channel, ReqId, Version, Flags); ++ {file_copy, AFile, BFile} -> ++ {ok, _} = file:copy(AFile, BFile) ++ end ++ end || ++ Action <- ++ [{?SSH_FX_OK, FileName, NewFileName, 0}, ++ {?SSH_FX_OK, NewFileName, FileName, ?SSH_FXP_RENAME_OVERWRITE}, ++ {file_copy, FileName, NewFileName}, ++ %% no overwrite ++ {?SSH_FX_FILE_ALREADY_EXISTS, FileName, NewFileName, ?SSH_FXP_RENAME_NATIVE}, ++ {?SSH_FX_OP_UNSUPPORTED, FileName, NewFileName, ?SSH_FXP_RENAME_ATOMIC}, ++ %% max_path ++ {?SSH_FX_NO_SUCH_PATH, FileName, LongFileName, 0}]], ++ ok. + + %%-------------------------------------------------------------------- + mk_rm_dir(Config) when is_list(Config) -> +@@ -1078,3 +1095,12 @@ encode_file_type(Type) -> + + not_default_permissions() -> + 8#600. %% User read-write-only ++ ++req_id() -> ++ ReqId = ++ case get(req_id) of ++ undefined -> 0; ++ I -> I ++ end, ++ put(req_id, ReqId + 1), ++ ReqId. diff --git a/debian/patches/CVE-2025-48040.patch b/debian/patches/CVE-2025-48040.patch new file mode 100644 index 0000000000..d27a182c56 --- /dev/null +++ b/debian/patches/CVE-2025-48040.patch @@ -0,0 +1,477 @@ +From: Jakub Witczak <[email protected]> +Date: Wed, 20 Aug 2025 10:30:55 +0200 +Subject: ssh: key exchange robustness improvements + +- reduce untrusted data processing for non-debug logs +- trim badmatch exceptions to avoid processing potentially malicious data +- terminate with kexinit_error when too many algorithms are received in KEX init message + +Origin: https://github.com/erlang/otp/commit/548f1295d86d0803da884db8685cc16d461d0d5a +Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2025-48040 +--- + lib/ssh/src/ssh_connection.erl | 3 +- + lib/ssh/src/ssh_connection_handler.erl | 35 +++++++--- + lib/ssh/src/ssh_lib.erl | 15 ++++- + lib/ssh/src/ssh_message.erl | 42 +++++++----- + lib/ssh/src/ssh_transport.erl | 120 +++++++++++++++++++-------------- + lib/ssh/test/ssh_connection_SUITE.erl | 12 +++- + 6 files changed, 147 insertions(+), 80 deletions(-) + +diff --git a/lib/ssh/src/ssh_connection.erl b/lib/ssh/src/ssh_connection.erl +index badf3a2..b21d249 100644 +--- a/lib/ssh/src/ssh_connection.erl ++++ b/lib/ssh/src/ssh_connection.erl +@@ -481,10 +481,9 @@ handle_msg(Msg, Connection, server, Ssh = #ssh{authenticated = false}) -> + %% respond by disconnecting, preferably with a proper disconnect message + %% sent to ease troubleshooting. + MsgFun = fun(M) -> +- MaxLogItemLen = ?GET_OPT(max_log_item_len, Ssh#ssh.opts), + io_lib:format("Connection terminated. Unexpected message for unauthenticated user." + " Message: ~w", [M], +- [{chars_limit, MaxLogItemLen}]) ++ [{chars_limit, ssh_lib:max_log_len(Ssh)}]) + end, + ?LOG_DEBUG(MsgFun, [Msg]), + {disconnect, {?SSH_DISCONNECT_PROTOCOL_ERROR, "Connection refused"}, handle_stop(Connection)}; +diff --git a/lib/ssh/src/ssh_connection_handler.erl b/lib/ssh/src/ssh_connection_handler.erl +index ba46468..fa3b374 100644 +--- a/lib/ssh/src/ssh_connection_handler.erl ++++ b/lib/ssh/src/ssh_connection_handler.erl +@@ -1146,12 +1146,21 @@ handle_event(info, {Proto, Sock, NewData}, StateName, + {next_event, internal, Msg} + ]} + catch +- C:E:ST -> +- MaxLogItemLen = ?GET_OPT(max_log_item_len,SshParams#ssh.opts), ++ Class:Reason0:Stacktrace -> ++ Reason = ssh_lib:trim_reason(Reason0), ++ MsgFun = ++ fun(debug) -> ++ io_lib:format("Bad packet: Decrypted, but can't decode~n~p:~p~n~p", ++ [Class,Reason,Stacktrace], ++ [{chars_limit, ssh_lib:max_log_len(SshParams)}]); ++ (_) -> ++ io_lib:format("Bad packet: Decrypted, but can't decode ~p:~p", ++ [Class, Reason], ++ [{chars_limit, ssh_lib:max_log_len(SshParams)}]) ++ end, + {Shutdown, D} = + ?send_disconnect(?SSH_DISCONNECT_PROTOCOL_ERROR, +- io_lib:format("Bad packet: Decrypted, but can't decode~n~p:~p~n~p", +- [C,E,ST], [{chars_limit, MaxLogItemLen}]), ++ ?SELECT_MSG(MsgFun), + StateName, D1), + {stop, Shutdown, D} + end; +@@ -1181,12 +1190,20 @@ handle_event(info, {Proto, Sock, NewData}, StateName, + StateName, D0), + {stop, Shutdown, D} + catch +- C:E:ST -> +- MaxLogItemLen = ?GET_OPT(max_log_item_len,SshParams#ssh.opts), ++ Class:Reason0:Stacktrace -> ++ MsgFun = ++ fun(debug) -> ++ io_lib:format("Bad packet: Couldn't decrypt~n~p:~p~n~p", ++ [Class,Reason0,Stacktrace], ++ [{chars_limit, ssh_lib:max_log_len(SshParams)}]); ++ (_) -> ++ Reason = ssh_lib:trim_reason(Reason0), ++ io_lib:format("Bad packet: Couldn't decrypt~n~p:~p", ++ [Class,Reason], ++ [{chars_limit, ssh_lib:max_log_len(SshParams)}]) ++ end, + {Shutdown, D} = +- ?send_disconnect(?SSH_DISCONNECT_PROTOCOL_ERROR, +- io_lib:format("Bad packet: Couldn't decrypt~n~p:~p~n~p", +- [C,E,ST], [{chars_limit, MaxLogItemLen}]), ++ ?send_disconnect(?SSH_DISCONNECT_PROTOCOL_ERROR, ?SELECT_MSG(MsgFun), + StateName, D0), + {stop, Shutdown, D} + end; +diff --git a/lib/ssh/src/ssh_lib.erl b/lib/ssh/src/ssh_lib.erl +index 3d29b5e..c6791f1 100644 +--- a/lib/ssh/src/ssh_lib.erl ++++ b/lib/ssh/src/ssh_lib.erl +@@ -28,7 +28,9 @@ + format_address_port/2, format_address_port/1, + format_address/1, + format_time_ms/1, +- comp/2 ++ comp/2, ++ trim_reason/1, ++ max_log_len/1 + ]). + + -include("ssh.hrl"). +@@ -86,3 +88,14 @@ comp([], [], Truth) -> + + comp(_, _, _) -> + false. ++%% We don't want to process badmatch details, potentially containing ++%% malicious data of unknown size ++trim_reason({badmatch, V}) when is_binary(V) -> ++ badmatch; ++trim_reason(E) -> ++ E. ++ ++max_log_len(#ssh{opts = Opts}) -> ++ ?GET_OPT(max_log_item_len, Opts); ++max_log_len(Opts) when is_map(Opts) -> ++ ?GET_OPT(max_log_item_len, Opts). +diff --git a/lib/ssh/src/ssh_message.erl b/lib/ssh/src/ssh_message.erl +index e22a4e2..4d5ac74 100644 +--- a/lib/ssh/src/ssh_message.erl ++++ b/lib/ssh/src/ssh_message.erl +@@ -43,7 +43,7 @@ + + -behaviour(ssh_dbg). + -export([ssh_dbg_trace_points/0, ssh_dbg_flags/1, ssh_dbg_on/1, ssh_dbg_off/1, ssh_dbg_format/2]). +--define(ALG_NAME_LIMIT, 64). ++-define(ALG_NAME_LIMIT, 64). % RFC4251 sec6 + + ucl(B) -> + try unicode:characters_to_list(B) of +@@ -821,23 +821,33 @@ decode_kex_init(<<?BYTE(Bool)>>, Acc, 0) -> + %% See rfc 4253 7.1 + X = 0, + list_to_tuple(lists:reverse([X, erl_boolean(Bool) | Acc])); +-decode_kex_init(<<?DEC_BIN(Data,__0), Rest/binary>>, Acc, N) -> ++decode_kex_init(<<?DEC_BIN(Data,__0), Rest/binary>>, Acc, N) when ++ byte_size(Data) < ?MAX_NUM_ALGORITHMS * ?ALG_NAME_LIMIT -> + BinParts = binary:split(Data, <<$,>>, [global]), +- Process = +- fun(<<>>, PAcc) -> +- PAcc; +- (Part, PAcc) -> +- case byte_size(Part) > ?ALG_NAME_LIMIT of +- true -> +- ?LOG_DEBUG("Ignoring too long name", []), ++ AlgCount = length(BinParts), ++ case AlgCount =< ?MAX_NUM_ALGORITHMS of ++ true -> ++ Process = ++ fun(<<>>, PAcc) -> + PAcc; +- false -> +- Name = binary:bin_to_list(Part), +- [Name | PAcc] +- end +- end, +- Names = lists:foldr(Process, [], BinParts), +- decode_kex_init(Rest, [Names | Acc], N - 1). ++ (Part, PAcc) -> ++ case byte_size(Part) =< ?ALG_NAME_LIMIT of ++ true -> ++ Name = binary:bin_to_list(Part), ++ [Name | PAcc]; ++ false -> ++ ?LOG_DEBUG("Ignoring too long name", []), ++ PAcc ++ end ++ end, ++ Names = lists:foldr(Process, [], BinParts), ++ decode_kex_init(Rest, [Names | Acc], N - 1); ++ false -> ++ throw({error, {kexinit_error, N, {alg_count, AlgCount}}}) ++ end; ++decode_kex_init(<<?DEC_BIN(Data,__0), _Rest/binary>>, _Acc, N) -> ++ throw({error, {kexinit, N, {string_size, byte_size(Data)}}}). ++ + + + %%%================================================================ +diff --git a/lib/ssh/src/ssh_transport.erl b/lib/ssh/src/ssh_transport.erl +index 6081a02..5abc11b 100644 +--- a/lib/ssh/src/ssh_transport.erl ++++ b/lib/ssh/src/ssh_transport.erl +@@ -405,8 +405,9 @@ handle_kexinit_msg(#ssh_msg_kexinit{} = CounterPart, #ssh_msg_kexinit{} = Own, + key_exchange_first_msg(Algos#alg.kex, + Ssh#ssh{algorithms = Algos}) + catch +- Class:Error -> +- Msg = kexinit_error(Class, Error, client, Own, CounterPart), ++ Class:Reason0 -> ++ Reason = ssh_lib:trim_reason(Reason0), ++ Msg = kexinit_error(Class, Reason, client, Own, CounterPart, Ssh), + ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, Msg) + end; + +@@ -422,31 +423,38 @@ handle_kexinit_msg(#ssh_msg_kexinit{} = CounterPart, #ssh_msg_kexinit{} = Own, + Algos -> + {ok, Ssh#ssh{algorithms = Algos}} + catch +- Class:Error -> +- Msg = kexinit_error(Class, Error, server, Own, CounterPart), ++ Class:Reason0 -> ++ Reason = ssh_lib:trim_reason(Reason0), ++ Msg = kexinit_error(Class, Reason, server, Own, CounterPart, Ssh), + ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, Msg) + end. + +-kexinit_error(Class, Error, Role, Own, CounterPart) -> ++kexinit_error(Class, Error, Role, Own, CounterPart, Ssh) -> + {Fmt,Args} = + case {Class,Error} of + {error, {badmatch,{false,Alg}}} -> + {Txt,W,C} = alg_info(Role, Alg), +- {"No common ~s algorithm,~n" +- " we have:~n ~s~n" +- " peer have:~n ~s~n", +- [Txt, +- lists:join(", ", element(W,Own)), +- lists:join(", ", element(C,CounterPart)) +- ]}; ++ MsgFun = ++ fun(debug) -> ++ {"No common ~s algorithm,~n" ++ " we have:~n ~s~n" ++ " peer have:~n ~s~n", ++ [Txt, ++ lists:join(", ", element(W,Own)), ++ lists:join(", ", element(C,CounterPart))]}; ++ (_) -> ++ {"No common ~s algorithm", [Txt]} ++ end, ++ ?SELECT_MSG(MsgFun); + _ -> + {"Kexinit failed in ~p: ~p:~p", [Role,Class,Error]} + end, +- try io_lib:format(Fmt, Args) of ++ try io_lib:format(Fmt, Args, [{chars_limit, ssh_lib:max_log_len(Ssh)}]) of + R -> R + catch + _:_ -> +- io_lib:format("Kexinit failed in ~p: ~p:~p", [Role, Class, Error]) ++ io_lib:format("Kexinit failed in ~p: ~p:~p", [Role, Class, Error], ++ [{chars_limit, ssh_lib:max_log_len(Ssh)}]) + end. + + alg_info(client, Alg) -> +@@ -598,14 +606,19 @@ handle_kexdh_init(#ssh_msg_kexdh_init{e = E}, + session_id = sid(Ssh1, H)}}; + {error,unsupported_sign_alg} -> + ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, +- io_lib:format("Unsupported algorithm ~p", [SignAlg]) +- ) ++ io_lib:format("Unsupported algorithm ~p", [SignAlg], ++ [{chars_limit, ssh_lib:max_log_len(Opts)}])) + end; + true -> +- ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, ++ MsgFun = ++ fun(debug) -> + io_lib:format("Kexdh init failed, received 'e' out of bounds~n E=~p~n P=~p", +- [E,P]) +- ) ++ [E,P], [{chars_limit, ssh_lib:max_log_len(Opts)}]); ++ (_) -> ++ io_lib:format("Kexdh init failed, received 'e' out of bounds", [], ++ [{chars_limit, ssh_lib:max_log_len(Opts)}] ) ++ end, ++ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, ?SELECT_MSG(MsgFun)) + end. + + handle_kexdh_reply(#ssh_msg_kexdh_reply{public_host_key = PeerPubHostKey, +@@ -626,14 +639,15 @@ handle_kexdh_reply(#ssh_msg_kexdh_reply{public_host_key = PeerPubHostKey, + session_id = sid(Ssh, H)})}; + Error -> + ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, +- io_lib:format("Kexdh init failed. Verify host key: ~p",[Error]) ++ io_lib:format("Kexdh init failed. Verify host key: ~p",[Error], ++ [{chars_limit, ssh_lib:max_log_len(Ssh0)}]) + ) + end; + + true -> + ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, + io_lib:format("Kexdh init failed, received 'f' out of bounds~n F=~p~n P=~p", +- [F,P]) ++ [F,P], [{chars_limit, ssh_lib:max_log_len(Ssh0)}]) + ) + end. + +@@ -659,7 +673,8 @@ handle_kex_dh_gex_request(#ssh_msg_kex_dh_gex_request{min = Min0, + }}; + {error,_} -> + ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, +- io_lib:format("No possible diffie-hellman-group-exchange group found",[]) ++ io_lib:format("No possible diffie-hellman-group-exchange group found",[], ++ [{chars_limit, ssh_lib:max_log_len(Opts)}]) + ) + end; + +@@ -691,8 +706,8 @@ handle_kex_dh_gex_request(#ssh_msg_kex_dh_gex_request_old{n = NBits}, + }}; + {error,_} -> + ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, +- io_lib:format("No possible diffie-hellman-group-exchange group found",[]) +- ) ++ io_lib:format("No possible diffie-hellman-group-exchange group found",[], ++ [{chars_limit, ssh_lib:max_log_len(Opts)}])) + end; + + handle_kex_dh_gex_request(_, _) -> +@@ -718,7 +733,6 @@ handle_kex_dh_gex_group(#ssh_msg_kex_dh_gex_group{p = P, g = G}, Ssh0) -> + {Public, Private} = generate_key(dh, [P,G,2*Sz]), + {SshPacket, Ssh1} = + ssh_packet(#ssh_msg_kex_dh_gex_init{e = Public}, Ssh0), % Pub = G^Priv mod P (def) +- + {ok, SshPacket, + Ssh1#ssh{keyex_key = {{Private, Public}, {G, P}}}}. + +@@ -749,19 +763,22 @@ handle_kex_dh_gex_init(#ssh_msg_kex_dh_gex_init{e = E}, + }}; + {error,unsupported_sign_alg} -> + ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, +- io_lib:format("Unsupported algorithm ~p", [SignAlg]) +- ) ++ io_lib:format("Unsupported algorithm ~p", [SignAlg], ++ [{chars_limit, ssh_lib:max_log_len(Opts)}])) + end; + true -> + ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, +- "Kexdh init failed, received 'k' out of bounds" +- ) ++ "Kexdh init failed, received 'k' out of bounds") + end; + true -> +- ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, +- io_lib:format("Kexdh gex init failed, received 'e' out of bounds~n E=~p~n P=~p", +- [E,P]) +- ) ++ MsgFun = ++ fun(debug) -> ++ io_lib:format("Kexdh gex init failed, received 'e' out of bounds~n" ++ " E=~p~n P=~p", [E,P]); ++ (_) -> ++ io_lib:format("Kexdh gex init failed, received 'e' out of bounds", []) ++ end, ++ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, ?SELECT_MSG(MsgFun)) + end. + + handle_kex_dh_gex_reply(#ssh_msg_kex_dh_gex_reply{public_host_key = PeerPubHostKey, +@@ -786,20 +803,18 @@ handle_kex_dh_gex_reply(#ssh_msg_kex_dh_gex_reply{public_host_key = PeerPubHostK + session_id = sid(Ssh, H)})}; + Error -> + ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, +- io_lib:format("Kexdh gex reply failed. Verify host key: ~p",[Error]) +- ) ++ io_lib:format("Kexdh gex reply failed. Verify host key: ~p", ++ [Error], [{chars_limit, ssh_lib:max_log_len(Ssh0)}])) + end; + + true -> + ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, +- "Kexdh gex init failed, 'K' out of bounds" +- ) ++ "Kexdh gex init failed, 'K' out of bounds") + end; + true -> + ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, + io_lib:format("Kexdh gex init failed, received 'f' out of bounds~n F=~p~n P=~p", +- [F,P]) +- ) ++ [F,P], [{chars_limit, ssh_lib:max_log_len(Ssh0)}])) + end. + + %%%---------------------------------------------------------------- +@@ -833,17 +848,25 @@ handle_kex_ecdh_init(#ssh_msg_kex_ecdh_init{q_c = PeerPublic}, + session_id = sid(Ssh1, H)}}; + {error,unsupported_sign_alg} -> + ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, +- io_lib:format("Unsupported algorithm ~p", [SignAlg]) +- ) ++ io_lib:format("Unsupported algorithm ~p", [SignAlg], ++ [{chars_limit, ssh_lib:max_log_len(Opts)}])) + end + catch +- Class:Error -> +- ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, ++ Class:Reason0 -> ++ Reason = ssh_lib:trim_reason(Reason0), ++ MsgFun = ++ fun(debug) -> + io_lib:format("ECDH compute key failed in server: ~p:~p~n" + "Kex: ~p, Curve: ~p~n" + "PeerPublic: ~p", +- [Class,Error,Kex,Curve,PeerPublic]) +- ) ++ [Class,Reason,Kex,Curve,PeerPublic], ++ [{chars_limit, ssh_lib:max_log_len(Ssh0)}]); ++ (_) -> ++ io_lib:format("ECDH compute key failed in server: ~p:~p", ++ [Class,Reason], ++ [{chars_limit, ssh_lib:max_log_len(Ssh0)}]) ++ end, ++ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, ?SELECT_MSG(MsgFun)) + end. + + handle_kex_ecdh_reply(#ssh_msg_kex_ecdh_reply{public_host_key = PeerPubHostKey, +@@ -866,15 +889,14 @@ handle_kex_ecdh_reply(#ssh_msg_kex_ecdh_reply{public_host_key = PeerPubHostKey, + session_id = sid(Ssh, H)})}; + Error -> + ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, +- io_lib:format("ECDH reply failed. Verify host key: ~p",[Error]) +- ) ++ io_lib:format("ECDH reply failed. Verify host key: ~p",[Error], ++ [{chars_limit, ssh_lib:max_log_len(Ssh0)}])) + end + catch + Class:Error -> + ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, + io_lib:format("Peer ECDH public key seem invalid: ~p:~p", +- [Class,Error]) +- ) ++ [Class,Error], [{chars_limit, ssh_lib:max_log_len(Ssh0)}])) + end. + + +diff --git a/lib/ssh/test/ssh_connection_SUITE.erl b/lib/ssh/test/ssh_connection_SUITE.erl +index 06d90cc..d529cf5 100644 +--- a/lib/ssh/test/ssh_connection_SUITE.erl ++++ b/lib/ssh/test/ssh_connection_SUITE.erl +@@ -1345,6 +1345,8 @@ gracefull_invalid_long_start_no_nl(Config) when is_list(Config) -> + end. + + kex_error(Config) -> ++ #{level := Level} = logger:get_primary_config(), ++ ok = logger:set_primary_config(level, debug), + PrivDir = proplists:get_value(priv_dir, Config), + UserDir = filename:join(PrivDir, nopubkey), % to make sure we don't use public-key-auth + file:make_dir(UserDir), +@@ -1365,6 +1367,10 @@ kex_error(Config) -> + ok % Other msg + end, + self()), ++ Cleanup = fun() -> ++ ok = logger:remove_handler(kex_error), ++ ok = logger:set_primary_config(level, Level) ++ end, + try + ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true}, + {user, "foo"}, +@@ -1382,7 +1388,7 @@ kex_error(Config) -> + %% ok + receive + {Ref, ErrMsgTxt} -> +- ok = logger:remove_handler(kex_error), ++ Cleanup(), + ct:log("ErrMsgTxt = ~n~s", [ErrMsgTxt]), + Lines = lists:map(fun string:trim/1, string:tokens(ErrMsgTxt, "\n")), + OK = (lists:all(fun(S) -> lists:member(S,Lines) end, +@@ -1400,12 +1406,12 @@ kex_error(Config) -> + ct:fail("unexpected error text msg", []) + end + after 20000 -> +- ok = logger:remove_handler(kex_error), ++ Cleanup(), + ct:fail("timeout", []) + end; + + error:{badmatch,{error,_}} -> +- ok = logger:remove_handler(kex_error), ++ Cleanup(), + ct:fail("unexpected error msg", []) + end. + diff --git a/debian/patches/CVE-2025-48041.patch b/debian/patches/CVE-2025-48041.patch new file mode 100644 index 0000000000..45ee9b7d0f --- /dev/null +++ b/debian/patches/CVE-2025-48041.patch @@ -0,0 +1,268 @@ +From: Jakub Witczak <[email protected]> +Date: Wed, 20 Aug 2025 10:31:50 +0200 +Subject: ssh: max_handles option added to ssh_sftpd + +- add max_handles option and update tests (1000 by default) +- remove sshd_read_file redundant testcase + +Origin: https://github.com/erlang/otp/commit/d49efa2d4fa9e6f7ee658719cd76ffe7a33c2401 +Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2025-48041 +--- + lib/ssh/doc/src/ssh_sftpd.xml | 4 ++ + lib/ssh/src/ssh_sftpd.erl | 30 +++++++++++---- + lib/ssh/test/ssh_sftpd_SUITE.erl | 83 ++++++++++++++++++---------------------- + 3 files changed, 64 insertions(+), 53 deletions(-) + +diff --git a/lib/ssh/doc/src/ssh_sftpd.xml b/lib/ssh/doc/src/ssh_sftpd.xml +index efabf3f..7c250a9 100644 +--- a/lib/ssh/doc/src/ssh_sftpd.xml ++++ b/lib/ssh/doc/src/ssh_sftpd.xml +@@ -65,6 +65,10 @@ + If supplied, the number of filenames returned to the SFTP client per <c>READDIR</c> + request is limited to at most the given value.</p> + </item> ++ <tag><c>max_handles</c></tag> ++ <item> ++ <p>The default value is <c>1000</c>. Positive integer value represents the maximum number of file handles allowed for a connection.</p> ++ </item> + <tag><c>max_path</c></tag> + <item> + <p>The default value is <c>4096</c>. Positive integer +diff --git a/lib/ssh/src/ssh_sftpd.erl b/lib/ssh/src/ssh_sftpd.erl +index 5632848..dfa566a 100644 +--- a/lib/ssh/src/ssh_sftpd.erl ++++ b/lib/ssh/src/ssh_sftpd.erl +@@ -52,6 +52,7 @@ + file_handler, % atom() - callback module + file_state, % state for the file callback module + max_files, % integer >= 0 max no files sent during READDIR ++ max_handles, % integer > 0 - max number of file handles + max_path, % integer > 0 - max length of path + options, % from the subsystem declaration + handles % list of open handles +@@ -66,6 +67,7 @@ + Options :: [ {cwd, string()} | + {file_handler, CbMod | {CbMod, FileState}} | + {max_files, integer()} | ++ {max_handles, integer()} | + {max_path, integer()} | + {root, string()} | + {sftpd_vsn, integer()} +@@ -117,11 +119,13 @@ init(Options) -> + {Root0, State0} + end, + MaxLength = proplists:get_value(max_files, Options, 0), ++ MaxHandles = proplists:get_value(max_handles, Options, 1000), + MaxPath = proplists:get_value(max_path, Options, 4096), + Vsn = proplists:get_value(sftpd_vsn, Options, 5), + {ok, State#state{cwd = CWD, + root = Root, + max_files = MaxLength, ++ max_handles = MaxHandles, + max_path = MaxPath, + options = Options, + handles = [], pending = <<>>, +@@ -286,14 +290,16 @@ handle_op(?SSH_FXP_REALPATH, ReqId, + end; + handle_op(?SSH_FXP_OPENDIR, ReqId, + <<?UINT32(RLen), RPath:RLen/binary>>, +- State0 = #state{xf = #ssh_xfer{vsn = Vsn}, +- file_handler = FileMod, file_state = FS0}) -> ++ State0 = #state{xf = #ssh_xfer{vsn = Vsn}, ++ file_handler = FileMod, file_state = FS0, ++ max_handles = MaxHandles}) -> + RelPath = unicode:characters_to_list(RPath), + AbsPath = relate_file_name(RelPath, State0), + + XF = State0#state.xf, + {IsDir, FS1} = FileMod:is_dir(AbsPath, FS0), + State1 = State0#state{file_state = FS1}, ++ HandlesCnt = length(State0#state.handles), + case IsDir of + false when Vsn > 5 -> + ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_NOT_A_DIRECTORY, +@@ -303,8 +309,12 @@ handle_op(?SSH_FXP_OPENDIR, ReqId, + ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_FAILURE, + "Not a directory"), + State1; +- true -> +- add_handle(State1, XF, ReqId, directory, {RelPath,unread}) ++ true when HandlesCnt < MaxHandles -> ++ add_handle(State1, XF, ReqId, directory, {RelPath,unread}); ++ true -> ++ ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_FAILURE, ++ "max_handles limit reached"), ++ State1 + end; + handle_op(?SSH_FXP_READDIR, ReqId, + <<?UINT32(HLen), BinHandle:HLen/binary>>, +@@ -755,7 +765,9 @@ open(Vsn, ReqId, Data, State) when Vsn >= 4 -> + do_open(ReqId, State, Path, Flags). + + do_open(ReqId, State0, Path, Flags) -> +- #state{file_handler = FileMod, file_state = FS0, xf = #ssh_xfer{vsn = Vsn}} = State0, ++ #state{file_handler = FileMod, file_state = FS0, xf = #ssh_xfer{vsn = Vsn}, ++ max_handles = MaxHandles} = State0, ++ HandlesCnt = length(State0#state.handles), + AbsPath = relate_file_name(Path, State0), + {IsDir, _FS1} = FileMod:is_dir(AbsPath, FS0), + case IsDir of +@@ -767,7 +779,7 @@ do_open(ReqId, State0, Path, Flags) -> + ssh_xfer:xf_send_status(State0#state.xf, ReqId, + ?SSH_FX_FAILURE, "File is a directory"), + State0; +- false -> ++ false when HandlesCnt < MaxHandles -> + OpenFlags = [binary | Flags], + {Res, FS1} = FileMod:open(AbsPath, OpenFlags, FS0), + State1 = State0#state{file_state = FS1}, +@@ -778,7 +790,11 @@ do_open(ReqId, State0, Path, Flags) -> + ssh_xfer:xf_send_status(State1#state.xf, ReqId, + ssh_xfer:encode_erlang_status(Error)), + State1 +- end ++ end; ++ false -> ++ ssh_xfer:xf_send_status(State0#state.xf, ReqId, ++ ?SSH_FX_FAILURE, "max_handles limit reached"), ++ State0 + end. + + %% resolve all symlinks in a path +diff --git a/lib/ssh/test/ssh_sftpd_SUITE.erl b/lib/ssh/test/ssh_sftpd_SUITE.erl +index f04cde3..fade45b 100644 +--- a/lib/ssh/test/ssh_sftpd_SUITE.erl ++++ b/lib/ssh/test/ssh_sftpd_SUITE.erl +@@ -52,7 +52,6 @@ + retrieve_attributes/1, + root_with_cwd/1, + set_attributes/1, +- sshd_read_file/1, + ver3_open_flags/1, + ver3_rename/1, + ver6_basic/1, +@@ -72,6 +71,7 @@ + -define(SSH_TIMEOUT, 10000). + -define(REG_ATTERS, <<0,0,0,0,1>>). + -define(UNIX_EPOCH, 62167219200). ++-define(MAX_HANDLES, 10). + -define(MAX_PATH, 200). + -define(is_set(F, Bits), ((F) band (Bits)) == (F)). + +@@ -98,8 +98,7 @@ all() -> + links, + ver3_rename, + ver3_open_flags, +- relpath, +- sshd_read_file, ++ relpath, + ver6_basic, + access_outside_root, + root_with_cwd, +@@ -182,7 +181,8 @@ init_per_testcase(TestCase, Config) -> + ssh:daemon(0, [{subsystems, SubSystems}|Options]); + _ -> + SubSystems = [ssh_sftpd:subsystem_spec( +- [{max_path, ?MAX_PATH}])], ++ [{max_handles, ?MAX_HANDLES}, ++ {max_path, ?MAX_PATH}])], + ssh:daemon(0, [{subsystems, SubSystems}|Options]) + end, + +@@ -318,22 +318,25 @@ open_close_dir(Config) when is_list(Config) -> + read_file(Config) when is_list(Config) -> + PrivDir = proplists:get_value(priv_dir, Config), + FileName = filename:join(PrivDir, "test.txt"), +- +- ReqId = 0, +- {Cm, Channel} = proplists:get_value(sftp, Config), +- +- {ok, <<?SSH_FXP_HANDLE, ?UINT32(ReqId), Handle/binary>>, _} = +- open_file(FileName, Cm, Channel, ReqId, +- ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES, +- ?SSH_FXF_OPEN_EXISTING), +- +- NewReqId = 1, +- +- {ok, <<?SSH_FXP_DATA, ?UINT32(NewReqId), ?UINT32(_Length), +- Data/binary>>, _} = +- read_file(Handle, 100, 0, Cm, Channel, NewReqId), +- +- {ok, Data} = file:read_file(FileName). ++ {Cm, Channel} = proplists:get_value(sftp, Config), ++ [begin ++ R1 = req_id(), ++ {ok, <<?SSH_FXP_HANDLE, ?UINT32(R1), Handle/binary>>, _} = ++ open_file(FileName, Cm, Channel, R1, ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES, ++ ?SSH_FXF_OPEN_EXISTING), ++ R2 = req_id(), ++ {ok, <<?SSH_FXP_DATA, ?UINT32(R2), ?UINT32(_Length), Data/binary>>, _} = ++ read_file(Handle, 100, 0, Cm, Channel, R2), ++ {ok, Data} = file:read_file(FileName) ++ end || _I <- lists:seq(0, ?MAX_HANDLES-1)], ++ ReqId = req_id(), ++ {ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId), ?UINT32(?SSH_FX_FAILURE), ++ ?UINT32(MsgLen), Msg:MsgLen/binary, ++ ?UINT32(LangTagLen), _LangTag:LangTagLen/binary>>, _} = ++ open_file(FileName, Cm, Channel, ReqId, ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES, ++ ?SSH_FXF_OPEN_EXISTING), ++ ct:log("Message: ~s", [Msg]), ++ ok. + + %%-------------------------------------------------------------------- + max_path(Config) when is_list(Config) -> +@@ -356,12 +359,21 @@ max_path(Config) when is_list(Config) -> + read_dir(Config) when is_list(Config) -> + PrivDir = proplists:get_value(priv_dir, Config), + {Cm, Channel} = proplists:get_value(sftp, Config), +- ReqId = 0, +- {ok, <<?SSH_FXP_HANDLE, ?UINT32(ReqId), Handle/binary>>, _} = +- open_dir(PrivDir, Cm, Channel, ReqId), +- ok = read_dir(Handle, Cm, Channel, ReqId). ++ [begin ++ R1 = req_id(), ++ {ok, <<?SSH_FXP_HANDLE, ?UINT32(R1), Handle/binary>>, _} = ++ open_dir(PrivDir, Cm, Channel, R1), ++ R2 = req_id(), ++ ok = read_dir(Handle, Cm, Channel, R2) ++ end || _I <- lists:seq(0, ?MAX_HANDLES-1)], ++ ReqId = req_id(), ++ {ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId), ?UINT32(?SSH_FX_FAILURE), ++ ?UINT32(MsgLen), Msg:MsgLen/binary, ++ ?UINT32(LangTagLen), _LangTag:LangTagLen/binary>>, _} = ++ open_dir(PrivDir, Cm, Channel, ReqId), ++ ct:log("Message: ~s", [Msg]), ++ ok. + +-%%-------------------------------------------------------------------- + write_file(Config) when is_list(Config) -> + PrivDir = proplists:get_value(priv_dir, Config), + FileName = filename:join(PrivDir, "test.txt"), +@@ -661,27 +673,6 @@ relpath(Config) when is_list(Config) -> + Root = Path + end. + +-%%-------------------------------------------------------------------- +-sshd_read_file(Config) when is_list(Config) -> +- PrivDir = proplists:get_value(priv_dir, Config), +- FileName = filename:join(PrivDir, "test.txt"), +- +- ReqId = 0, +- {Cm, Channel} = proplists:get_value(sftp, Config), +- +- {ok, <<?SSH_FXP_HANDLE, ?UINT32(ReqId), Handle/binary>>, _} = +- open_file(FileName, Cm, Channel, ReqId, +- ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES, +- ?SSH_FXF_OPEN_EXISTING), +- +- NewReqId = 1, +- +- {ok, <<?SSH_FXP_DATA, ?UINT32(NewReqId), ?UINT32(_Length), +- Data/binary>>, _} = +- read_file(Handle, 100, 0, Cm, Channel, NewReqId), +- +- {ok, Data} = file:read_file(FileName). +-%%-------------------------------------------------------------------- + ver6_basic(Config) when is_list(Config) -> + PrivDir = proplists:get_value(priv_dir, Config), + %FileName = filename:join(PrivDir, "test.txt"), diff --git a/debian/patches/series b/debian/patches/series index 405ebf00f2..2fdd8154c8 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -16,3 +16,7 @@ ssh-early-RCE-fix.patch ssh-strict-KEX-exchange-hardening.patch zip-sanitize-paths.patch xslt-for-each.patch +CVE-2025-48038.patch +CVE-2025-48039.patch +CVE-2025-48040.patch +CVE-2025-48041.patch diff --git a/debian/salsa-ci.yml b/debian/salsa-ci.yml new file mode 100644 index 0000000000..f437b7ffa5 --- /dev/null +++ b/debian/salsa-ci.yml @@ -0,0 +1,6 @@ +--- +include: + - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/recipes/debian.yml + +variables: + RELEASE: 'bookworm'

