Version in base suite: 25.2.3+dfsg-1 Base version: erlang_25.2.3+dfsg-1 Target version: erlang_25.2.3+dfsg-1+deb12u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/e/erlang/erlang_25.2.3+dfsg-1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/e/erlang/erlang_25.2.3+dfsg-1+deb12u1.dsc changelog | 19 patches/series | 7 patches/ssh-KEX-strict.patch | 797 ++++++++++ patches/ssh-custom_kexinit-test-added.patch | 136 + patches/ssh-early-RCE-fix.patch | 212 ++ patches/ssh-ignore-too-long-names.patch | 53 patches/ssh-reduce-log-processing-for-plain-connections.patch | 133 + patches/ssh-sftp-reject-packets-exceeding-limit.patch | 83 + patches/ssh-use-chars_limit-for-bad-packets-error-messages.patch | 33 9 files changed, 1473 insertions(+) diff -Nru erlang-25.2.3+dfsg/debian/changelog erlang-25.2.3+dfsg/debian/changelog --- erlang-25.2.3+dfsg/debian/changelog 2023-02-17 14:47:23.000000000 +0000 +++ erlang-25.2.3+dfsg/debian/changelog 2025-04-20 05:09:59.000000000 +0000 @@ -1,3 +1,22 @@ +erlang (1:25.2.3+dfsg-1+deb12u1) bookworm-security; urgency=high + + [ Salvatore Bonaccorso ] + * ssh: implement strict KEX (CVE-2023-48795) (Closes: #1059002) + * ssh: reject SFTP packets exceeding max allowed size (CVE-2025-26618) + * ssh: fix denial of service due to erroneous processing of large KEX + init packages (CVE-2025-30211) (Closes: #1101713): + - reduce log processing for plain connections + - ignore too long algorithm names + - limit the length of error messages in reply to invalid packets + - add the custom_kexinit test to test large KEX init packages processing + * ssh: fix remote code execution (RCE) by an unauthenticated user + (CVE-2025-32433) (Closes: #1103442) + + [ Sergei Golovan ] + * Cleanup the patches. + + -- Sergei Golovan Sun, 20 Apr 2025 08:09:59 +0300 + erlang (1:25.2.3+dfsg-1) unstable; urgency=medium * New upstream release. diff -Nru erlang-25.2.3+dfsg/debian/patches/series erlang-25.2.3+dfsg/debian/patches/series --- erlang-25.2.3+dfsg/debian/patches/series 2023-02-17 14:47:23.000000000 +0000 +++ erlang-25.2.3+dfsg/debian/patches/series 2025-04-20 05:09:59.000000000 +0000 @@ -6,3 +6,10 @@ java.patch javascript.patch x32.patch +ssh-KEX-strict.patch +ssh-sftp-reject-packets-exceeding-limit.patch +ssh-reduce-log-processing-for-plain-connections.patch +ssh-ignore-too-long-names.patch +ssh-use-chars_limit-for-bad-packets-error-messages.patch +ssh-custom_kexinit-test-added.patch +ssh-early-RCE-fix.patch diff -Nru erlang-25.2.3+dfsg/debian/patches/ssh-KEX-strict.patch erlang-25.2.3+dfsg/debian/patches/ssh-KEX-strict.patch --- erlang-25.2.3+dfsg/debian/patches/ssh-KEX-strict.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-25.2.3+dfsg/debian/patches/ssh-KEX-strict.patch 2025-04-20 05:09:59.000000000 +0000 @@ -0,0 +1,797 @@ +From: Jakub Witczak +Date: Fri, 15 Dec 2023 09:12:33 +0100 +Subject: ssh: KEX strict + The patch strictenes KEX to avoid disabling or downgrading + security features for the connection. + - negotiate "strict KEX" OpenSSH feature + - when negotiated between peers apply strict KEX + - related tests + - print_seqnums fix in ssh_trtp test code +Origin: https://github.com/erlang/otp/commit/ee67d46285394db95133709cef74b0c462d665aa +Bug-Debian: https://bugs.debian.org/1059002 +Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2023-48795 + +--- a/lib/ssh/src/ssh.hrl ++++ b/lib/ssh/src/ssh.hrl +@@ -443,6 +443,8 @@ + send_ext_info, %% May send ext-info to peer + recv_ext_info, %% Expect ext-info from peer + ++ kex_strict_negotiated = false, ++ + algorithms, %% #alg{} + + send_mac = none, %% send MAC algorithm +@@ -514,7 +516,8 @@ + c_lng, + s_lng, + send_ext_info, +- recv_ext_info ++ recv_ext_info, ++ kex_strict_negotiated = false + }). + + -record(ssh_pty, {c_version = "", % client version string, e.g "SSH-2.0-Erlang/4.10.5" +--- a/lib/ssh/src/ssh_connection_handler.erl ++++ b/lib/ssh/src/ssh_connection_handler.erl +@@ -701,6 +701,16 @@ + disconnect_fun("Received disconnect: "++Desc, D), + {stop_and_reply, {shutdown,Desc}, Actions, D}; + ++handle_event(internal, #ssh_msg_ignore{}, {_StateName, _Role, init}, ++ #data{ssh_params = #ssh{kex_strict_negotiated = true, ++ send_sequence = SendSeq, ++ recv_sequence = RecvSeq}}) -> ++ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, ++ io_lib:format("strict KEX violation: unexpected SSH_MSG_IGNORE " ++ "send_sequence = ~p recv_sequence = ~p", ++ [SendSeq, RecvSeq]) ++ ); ++ + handle_event(internal, #ssh_msg_ignore{}, _StateName, _) -> + keep_state_and_data; + +--- a/lib/ssh/src/ssh_fsm_kexinit.erl ++++ b/lib/ssh/src/ssh_fsm_kexinit.erl +@@ -58,7 +58,7 @@ + handle_event(internal, {#ssh_msg_kexinit{}=Kex, Payload}, {kexinit,Role,ReNeg}, + D = #data{key_exchange_init_msg = OwnKex}) -> + Ssh1 = ssh_transport:key_init(peer_role(Role), D#data.ssh_params, Payload), +- Ssh = case ssh_transport:handle_kexinit_msg(Kex, OwnKex, Ssh1) of ++ Ssh = case ssh_transport:handle_kexinit_msg(Kex, OwnKex, Ssh1, ReNeg) of + {ok, NextKexMsg, Ssh2} when Role==client -> + ssh_connection_handler:send_bytes(NextKexMsg, D), + Ssh2; +--- a/lib/ssh/src/ssh_transport.erl ++++ b/lib/ssh/src/ssh_transport.erl +@@ -42,7 +42,7 @@ + key_exchange_init_msg/1, + key_init/3, new_keys_message/1, + ext_info_message/1, +- handle_kexinit_msg/3, handle_kexdh_init/2, ++ handle_kexinit_msg/4, handle_kexdh_init/2, + handle_kex_dh_gex_group/2, handle_kex_dh_gex_init/2, handle_kex_dh_gex_reply/2, + handle_new_keys/2, handle_kex_dh_gex_request/2, + handle_kexdh_reply/2, +@@ -236,7 +236,6 @@ + same( + select_crypto_supported( + [ +- {'chacha20-poly1305@openssh.com', [{ciphers,chacha20}, {macs,poly1305}]}, + {'aes256-gcm@openssh.com', [{ciphers,aes_256_gcm}]}, + {'aes256-ctr', [{ciphers,aes_256_ctr}]}, + {'aes192-ctr', [{ciphers,aes_192_ctr}]}, +@@ -244,6 +243,7 @@ + {'aes128-ctr', [{ciphers,aes_128_ctr}]}, + {'AEAD_AES_256_GCM', [{ciphers,aes_256_gcm}]}, + {'AEAD_AES_128_GCM', [{ciphers,aes_128_gcm}]}, ++ {'chacha20-poly1305@openssh.com', [{ciphers,chacha20}, {macs,poly1305}]}, + {'aes256-cbc', [{ciphers,aes_256_cbc}]}, + {'aes192-cbc', [{ciphers,aes_192_cbc}]}, + {'aes128-cbc', [{ciphers,aes_128_cbc}]}, +@@ -359,7 +359,8 @@ + #ssh_msg_kexinit{ + cookie = Random, + kex_algorithms = to_strings( get_algs(kex,Algs) ) +- ++ kex_ext_info(Role,Opts), ++ ++ kex_ext_info(Role,Opts) ++ ++ kex_strict_alg(Role), + server_host_key_algorithms = HostKeyAlgs, + encryption_algorithms_client_to_server = c2s(cipher,Algs), + encryption_algorithms_server_to_client = s2c(cipher,Algs), +@@ -388,10 +389,12 @@ + + + handle_kexinit_msg(#ssh_msg_kexinit{} = CounterPart, #ssh_msg_kexinit{} = Own, +- #ssh{role = client} = Ssh) -> ++ #ssh{role = client} = Ssh, ReNeg) -> + try +- {ok, Algorithms} = select_algorithm(client, Own, CounterPart, Ssh#ssh.opts), ++ {ok, Algorithms} = ++ select_algorithm(client, Own, CounterPart, Ssh, ReNeg), + true = verify_algorithm(Algorithms), ++ true = verify_kexinit_is_first_msg(Algorithms, Ssh, ReNeg), + Algorithms + of + Algos -> +@@ -404,10 +407,12 @@ + end; + + handle_kexinit_msg(#ssh_msg_kexinit{} = CounterPart, #ssh_msg_kexinit{} = Own, +- #ssh{role = server} = Ssh) -> ++ #ssh{role = server} = Ssh, ReNeg) -> + try +- {ok, Algorithms} = select_algorithm(server, CounterPart, Own, Ssh#ssh.opts), ++ {ok, Algorithms} = ++ select_algorithm(server, CounterPart, Own, Ssh, ReNeg), + true = verify_algorithm(Algorithms), ++ true = verify_kexinit_is_first_msg(Algorithms, Ssh, ReNeg), + Algorithms + of + Algos -> +@@ -488,6 +493,21 @@ + false -> {false, "kex"} + end. + ++verify_kexinit_is_first_msg(#alg{kex_strict_negotiated = false}, _, _) -> ++ true; ++verify_kexinit_is_first_msg(#alg{kex_strict_negotiated = true}, _, renegotiate) -> ++ true; ++verify_kexinit_is_first_msg(#alg{kex_strict_negotiated = true}, ++ #ssh{send_sequence = 1, recv_sequence = 1}, ++ init) -> ++ true; ++verify_kexinit_is_first_msg(#alg{kex_strict_negotiated = true}, ++ #ssh{send_sequence = SendSequence, ++ recv_sequence = RecvSequence}, init) -> ++ error_logger:warning_report( ++ lists:concat(["KEX strict violation (", SendSequence, ", ", RecvSequence, ")."])), ++ {false, "kex_strict"}. ++ + %%%---------------------------------------------------------------- + %%% + %%% Key exchange initialization +@@ -867,6 +887,9 @@ + ) + end. + ++%%%---------------------------------------------------------------- ++kex_strict_alg(client) -> [?kex_strict_c]; ++kex_strict_alg(server) -> [?kex_strict_s]. + + %%%---------------------------------------------------------------- + kex_ext_info(Role, Opts) -> +@@ -1057,7 +1080,35 @@ + %% + %% The first algorithm in each list MUST be the preferred (guessed) + %% algorithm. Each string MUST contain at least one algorithm name. +-select_algorithm(Role, Client, Server, Opts) -> ++select_algorithm(Role, Client, Server, ++ #ssh{opts = Opts, ++ kex_strict_negotiated = KexStrictNegotiated0}, ++ ReNeg) -> ++ KexStrictNegotiated = ++ case ReNeg of ++ %% KEX strict negotiated once per connection ++ init -> ++ Result = ++ case Role of ++ server -> ++ lists:member(?kex_strict_c, ++ Client#ssh_msg_kexinit.kex_algorithms); ++ client -> ++ lists:member(?kex_strict_s, ++ Server#ssh_msg_kexinit.kex_algorithms) ++ end, ++ case Result of ++ true -> ++ error_logger:info_report( ++ lists:concat([Role, " will use strict KEX ordering"])); ++ _ -> ++ ok ++ end, ++ Result; ++ _ -> ++ KexStrictNegotiated0 ++ end, ++ + {Encrypt0, Decrypt0} = select_encrypt_decrypt(Role, Client, Server), + {SendMac0, RecvMac0} = select_send_recv_mac(Role, Client, Server), + +@@ -1108,7 +1159,8 @@ + c_lng = C_Lng, + s_lng = S_Lng, + send_ext_info = SendExtInfo, +- recv_ext_info = RecvExtInfo ++ recv_ext_info = RecvExtInfo, ++ kex_strict_negotiated = KexStrictNegotiated + }}. + + +@@ -1206,7 +1258,8 @@ + c_lng = ALG#alg.c_lng, + s_lng = ALG#alg.s_lng, + send_ext_info = ALG#alg.send_ext_info, +- recv_ext_info = ALG#alg.recv_ext_info ++ recv_ext_info = ALG#alg.recv_ext_info, ++ kex_strict_negotiated = ALG#alg.kex_strict_negotiated + }; + + alg_setup(rcv, SSH) -> +@@ -1218,22 +1271,23 @@ + c_lng = ALG#alg.c_lng, + s_lng = ALG#alg.s_lng, + send_ext_info = ALG#alg.send_ext_info, +- recv_ext_info = ALG#alg.recv_ext_info ++ recv_ext_info = ALG#alg.recv_ext_info, ++ kex_strict_negotiated = ALG#alg.kex_strict_negotiated + }. + +- +-alg_init(snd, SSH0) -> ++alg_init(Dir = snd, SSH0) -> + {ok,SSH1} = send_mac_init(SSH0), + {ok,SSH2} = encrypt_init(SSH1), + {ok,SSH3} = compress_init(SSH2), +- SSH3; ++ {ok,SSH4} = maybe_reset_sequence(Dir, SSH3), ++ SSH4; + +-alg_init(rcv, SSH0) -> ++alg_init(Dir = rcv, SSH0) -> + {ok,SSH1} = recv_mac_init(SSH0), + {ok,SSH2} = decrypt_init(SSH1), + {ok,SSH3} = decompress_init(SSH2), +- SSH3. +- ++ {ok,SSH4} = maybe_reset_sequence(Dir, SSH3), ++ SSH4. + + alg_final(snd, SSH0) -> + {ok,SSH1} = send_mac_final(SSH0), +@@ -2198,6 +2252,14 @@ + + same(Algs) -> [{client2server,Algs}, {server2client,Algs}]. + ++maybe_reset_sequence(snd, Ssh = #ssh{kex_strict_negotiated = true}) -> ++ {ok, Ssh#ssh{send_sequence = 0}}; ++maybe_reset_sequence(rcv, Ssh = #ssh{kex_strict_negotiated = true}) -> ++ {ok, Ssh#ssh{recv_sequence = 0}}; ++maybe_reset_sequence(_Dir, Ssh) -> ++ {ok, Ssh}. ++ ++ + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + %% + %% Other utils +@@ -2224,14 +2286,14 @@ + ssh_dbg_flags(ssh_messages) -> ssh_dbg_flags(hello). + + +-ssh_dbg_on(alg) -> dbg:tpl(?MODULE,select_algorithm,4,x); ++ssh_dbg_on(alg) -> dbg:tpl(?MODULE,select_algorithm,5,x); + ssh_dbg_on(hello) -> dbg:tp(?MODULE,hello_version_msg,1,x), + dbg:tp(?MODULE,handle_hello_version,1,x); + ssh_dbg_on(raw_messages) -> ssh_dbg_on(hello); + ssh_dbg_on(ssh_messages) -> ssh_dbg_on(hello). + + +-ssh_dbg_off(alg) -> dbg:ctpl(?MODULE,select_algorithm,4); ++ssh_dbg_off(alg) -> dbg:ctpl(?MODULE,select_algorithm,5); + ssh_dbg_off(hello) -> dbg:ctpg(?MODULE,hello_version_msg,1), + dbg:ctpg(?MODULE,handle_hello_version,1); + ssh_dbg_off(raw_messages) -> ssh_dbg_off(hello); +@@ -2254,9 +2316,9 @@ + ssh_dbg_format(hello, {return_from,{?MODULE,handle_hello_version,1},_Ret}) -> + skip; + +-ssh_dbg_format(alg, {call,{?MODULE,select_algorithm,[_,_,_,_]}}) -> ++ssh_dbg_format(alg, {call,{?MODULE,select_algorithm,[_,_,_,_,_]}}) -> + skip; +-ssh_dbg_format(alg, {return_from,{?MODULE,select_algorithm,4},{ok,Alg}}) -> ++ssh_dbg_format(alg, {return_from,{?MODULE,select_algorithm,5},{ok,Alg}}) -> + ["Negotiated algorithms:\n", + wr_record(Alg) + ]; +--- a/lib/ssh/src/ssh_transport.hrl ++++ b/lib/ssh/src/ssh_transport.hrl +@@ -266,5 +266,7 @@ + -define(dh_group18, + {2, 16#FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C93402849236C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BDF8FF9406AD9E530EE5DB382F413001AEB06A53ED9027D831179727B0865A8918DA3EDBEBCF9B14ED44CE6CBACED4BB1BDB7F1447E6CC254B332051512BD7AF426FB8F401378CD2BF5983CA01C64B92ECF032EA15D1721D03F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E59E7C97FBEC7E8F323A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AACC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE32806A1D58BB7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55CDA56C9EC2EF29632387FE8D76E3C0468043E8F663F4860EE12BF2D5B0B7474D6E694F91E6DBE115974A3926F12FEE5E438777CB6A932DF8CD8BEC4D073B931BA3BC832B68D9DD300741FA7BF8AFC47ED2576F6936BA424663AAB639C5AE4F5683423B4742BF1C978238F16CBE39D652DE3FDB8BEFC848AD922222E04A4037C0713EB57A81A23F0C73473FC646CEA306B4BCBC8862F8385DDFA9D4B7FA2C087E879683303ED5BDD3A062B3CF5B3A278A66D2A13F83F44F82DDF310EE074AB6A364597E899A0255DC164F31CC50846851DF9AB48195DED7EA1B1D510BD7EE74D73FAF36BC31ECFA268359046F4EB879F924009438B481C6CD7889A002ED5EE382BC9190DA6FC026E479558E4475677E9AA9E3050E2765694DFC81F56E880B96E7160C980DD98EDD3DFFFFFFFFFFFFFFFFF}). + +- ++%%% OpenSSH KEX strict ++-define(kex_strict_c, "kex-strict-c-v00@openssh.com"). ++-define(kex_strict_s, "kex-strict-s-v00@openssh.com"). + -endif. % -ifdef(ssh_transport). +--- a/lib/ssh/test/ssh_protocol_SUITE.erl ++++ b/lib/ssh/test/ssh_protocol_SUITE.erl +@@ -53,6 +53,9 @@ + empty_service_name/1, + ext_info_c/1, + ext_info_s/1, ++ kex_strict_negotiated/1, ++ kex_strict_msg_ignore/1, ++ kex_strict_msg_unknown/1, + gex_client_init_option_groups/1, + gex_client_init_option_groups_file/1, + gex_client_init_option_groups_moduli_file/1, +@@ -136,8 +139,10 @@ + gex_client_init_option_groups_moduli_file, + gex_client_init_option_groups_file, + gex_client_old_request_exact, +- gex_client_old_request_noexact +- ]}, ++ gex_client_old_request_noexact, ++ kex_strict_negotiated, ++ kex_strict_msg_ignore, ++ kex_strict_msg_unknown]}, + {service_requests, [], [bad_service_name, + bad_long_service_name, + bad_very_long_service_name, +@@ -164,17 +169,16 @@ + + init_per_suite(Config) -> + ?CHECK_CRYPTO(start_std_daemon( setup_dirs( start_apps(Config)))). +- ++ + end_per_suite(Config) -> + stop_apps(Config). + +- +- + init_per_testcase(no_common_alg_server_disconnects, Config) -> + start_std_daemon(Config, [{preferred_algorithms,[{public_key,['ssh-rsa']}, + {cipher,?DEFAULT_CIPHERS} + ]}]); +- ++init_per_testcase(kex_strict_negotiated, Config) -> ++ Config; + init_per_testcase(TC, Config) when TC == gex_client_init_option_groups ; + TC == gex_client_init_option_groups_moduli_file ; + TC == gex_client_init_option_groups_file ; +@@ -217,6 +221,8 @@ + + end_per_testcase(no_common_alg_server_disconnects, Config) -> + stop_std_daemon(Config); ++end_per_testcase(kex_strict_negotiated, Config) -> ++ Config; + end_per_testcase(TC, Config) when TC == gex_client_init_option_groups ; + TC == gex_client_init_option_groups_moduli_file ; + TC == gex_client_init_option_groups_file ; +@@ -818,6 +824,80 @@ + {result, Pid, Error} -> ct:fail("Error: ~p",[Error]) + end. + ++%%%-------------------------------------------------------------------- ++%%% ++kex_strict_negotiated(Config0) -> ++ {ok,Pid} = ssh_test_lib:add_report_handler(), ++ Config = start_std_daemon(Config0, []), ++ {Server, Host, Port} = proplists:get_value(server, Config), ++ #{level := Level} = logger:get_primary_config(), ++ logger:set_primary_config(level, notice), ++ {ok, ConnRef} = std_connect({Host, Port}, Config, []), ++ {algorithms, A} = ssh:connection_info(ConnRef, algorithms), ++ ssh:stop_daemon(Server), ++ {ok, Reports} = ssh_test_lib:get_reports(Pid), ++ ct:log("Reports = ~p", [Reports]), ++ true = ssh_test_lib:kex_strict_negotiated(client, Reports), ++ true = ssh_test_lib:kex_strict_negotiated(server, Reports), ++ logger:set_primary_config(Level), ++ ok. ++ ++%% Connect to an erlang server and inject unexpected SSH ignore ++kex_strict_msg_ignore(Config) -> ++ ct:log("START: ~p~n=================================", [?FUNCTION_NAME]), ++ ExpectedReason = "strict KEX violation: unexpected SSH_MSG_IGNORE", ++ TestMessages = ++ [{send, ssh_msg_ignore}, ++ {match, #ssh_msg_kexdh_reply{_='_'}, receive_msg}, ++ {match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}], ++ kex_strict_helper(Config, TestMessages, ExpectedReason). ++ ++%% Connect to an erlang server and inject unexpected non-SSH binary ++kex_strict_msg_unknown(Config) -> ++ ct:log("START: ~p~n=================================", [?FUNCTION_NAME]), ++ ExpectedReason = "Bad packet: Size", ++ TestMessages = ++ [{send, ssh_msg_unknown}, ++ {match, #ssh_msg_kexdh_reply{_='_'}, receive_msg}, ++ {match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}], ++ kex_strict_helper(Config, TestMessages, ExpectedReason). ++ ++kex_strict_helper(Config, TestMessages, ExpectedReason) -> ++ {ok,HandlerPid} = ssh_test_lib:add_report_handler(), ++ #{level := Level} = logger:get_primary_config(), ++ logger:set_primary_config(level, notice), ++ %% Connect and negotiate keys ++ {ok, InitialState} = ssh_trpt_test_lib:exec( ++ [{set_options, [print_ops, print_seqnums, print_messages]}] ++ ), ++ {ok, _AfterKexState} = ++ ssh_trpt_test_lib:exec( ++ [{connect, ++ server_host(Config),server_port(Config), ++ [{preferred_algorithms,[{kex,[?DEFAULT_KEX]}, ++ {cipher,?DEFAULT_CIPHERS} ++ ]}, ++ {silently_accept_hosts, true}, ++ {recv_ext_info, false}, ++ {user_dir, user_dir(Config)}, ++ {user_interaction, false} ++ | proplists:get_value(extra_options,Config,[]) ++ ]}, ++ receive_hello, ++ {send, hello}, ++ {send, ssh_msg_kexinit}, ++ {match, #ssh_msg_kexinit{_='_'}, receive_msg}, ++ {send, ssh_msg_kexdh_init}] ++ ++ TestMessages, ++ InitialState), ++ ct:sleep(100), ++ {ok, Reports} = ssh_test_lib:get_reports(HandlerPid), ++ ct:log("HandlerPid = ~p~nReports = ~p", [HandlerPid, Reports]), ++ true = ssh_test_lib:kex_strict_negotiated(client, Reports), ++ true = ssh_test_lib:kex_strict_negotiated(server, Reports), ++ true = ssh_test_lib:event_logged(server, Reports, ExpectedReason), ++ logger:set_primary_config(Level), ++ ok. + + %%%---------------------------------------------------------------- + %%% +@@ -839,7 +919,7 @@ + Ciphers = filter_supported(cipher, ?CIPHERS), + {ok,_} = + chk_pref_algs(Config, +- [?DEFAULT_KEX, ?EXTRA_KEX], ++ [?DEFAULT_KEX, ?EXTRA_KEX, list_to_atom(?kex_strict_s)], + Ciphers, + [{preferred_algorithms, [{kex,[?DEFAULT_KEX]}, + {cipher,Ciphers} +@@ -853,7 +933,7 @@ + Ciphers = filter_supported(cipher, ?CIPHERS), + {ok,_} = + chk_pref_algs(Config, +- [?EXTRA_KEX, ?DEFAULT_KEX], ++ [?EXTRA_KEX, ?DEFAULT_KEX, list_to_atom(?kex_strict_s)], + Ciphers, + [{preferred_algorithms, [{kex,[?DEFAULT_KEX]}, + {cipher,Ciphers} +@@ -867,7 +947,7 @@ + Ciphers = filter_supported(cipher, ?CIPHERS), + {ok,_} = + chk_pref_algs(Config, +- [?DEFAULT_KEX], ++ [?DEFAULT_KEX, list_to_atom(?kex_strict_s)], + tl(Ciphers), + [{preferred_algorithms, [{kex,[?DEFAULT_KEX,?EXTRA_KEX]}, + {cipher,Ciphers} +@@ -886,7 +966,7 @@ + LastC = lists:last(Ciphers), + {ok,_} = + chk_pref_algs(Config, +- [?DEFAULT_KEX], ++ [?DEFAULT_KEX, list_to_atom(?kex_strict_s)], + [LastC] ++ (tl(Ciphers)--[LastC]) ++ [hd(Ciphers)], + [{preferred_algorithms, [{kex,[?DEFAULT_KEX,?EXTRA_KEX]}, + {cipher,Ciphers} +--- a/lib/ssh/test/ssh_test_lib.erl ++++ b/lib/ssh/test/ssh_test_lib.erl +@@ -121,7 +121,11 @@ + setup_host_key/3, + setup_known_host/3, + get_addr_str/0, +-file_base_name/2 ++file_base_name/2, ++add_report_handler/0, ++get_reports/1, ++kex_strict_negotiated/2, ++event_logged/3 + ]). + + -include_lib("common_test/include/ct.hrl"). +@@ -1267,3 +1271,49 @@ + file_base_name(system_src, Alg) -> file_base_name(system, Alg). + + %%%---------------------------------------------------------------- ++add_report_handler() -> ++ ssh_eqc_event_handler:add_report_handler(). ++ ++get_reports(Pid) -> ++ ssh_eqc_event_handler:get_reports(Pid). ++ ++-define(SEARCH_FUN(EXP), ++ begin ++ fun({info_report, _, {_, std_info, EXP}}) -> ++ true; ++ (_) -> ++ false ++ end ++ end). ++-define(SEARCH_SUFFIX, " will use strict KEX ordering"). ++ ++kex_strict_negotiated(client, Reports) -> ++ kex_strict_negotiated(?SEARCH_FUN("client" ++ ?SEARCH_SUFFIX), Reports); ++kex_strict_negotiated(server, Reports) -> ++ kex_strict_negotiated(?SEARCH_FUN("server" ++ ?SEARCH_SUFFIX), Reports); ++kex_strict_negotiated(SearchFun, Reports) when is_function(SearchFun) -> ++ case lists:search(SearchFun, Reports) of ++ {value, _} -> true; ++ _ -> false ++ end. ++ ++event_logged(Role, Reports, Reason) -> ++ SearchF = ++ fun({info_msg, _, {_, _Format, Args}}) -> ++ AnyF = fun (E) when is_list(E) -> ++ case string:find(E, Reason) of ++ nomatch -> false; ++ _ -> true ++ end; ++ (_) -> ++ false ++ end, ++ lists:member(Role, Args) andalso ++ lists:any(AnyF, Args); ++ (_) -> ++ false ++ end, ++ case lists:search(SearchF, Reports) of ++ {value, _} -> true; ++ _ -> false ++ end. +--- a/lib/ssh/test/ssh_to_openssh_SUITE.erl ++++ b/lib/ssh/test/ssh_to_openssh_SUITE.erl +@@ -23,6 +23,7 @@ + + -include_lib("common_test/include/ct.hrl"). + -include("ssh_test_lib.hrl"). ++-include_lib("ssh/src/ssh_transport.hrl"). + + -export([ + suite/0, +@@ -38,7 +39,9 @@ + + -export([ + erlang_server_openssh_client_renegotiate/1, ++ eserver_oclient_kex_strict/1, + erlang_shell_client_openssh_server/1, ++ eclient_oserver_kex_strict/1, + exec_direct_with_io_in_sshc/1, + exec_with_io_in_sshc/1, + tunnel_in_erlclient_erlserver/1, +@@ -73,12 +76,14 @@ + [{erlang_client, [], [tunnel_in_erlclient_erlserver, + tunnel_out_erlclient_erlserver, + {group, tunnel_distro_server}, +- erlang_shell_client_openssh_server ++ erlang_shell_client_openssh_server, ++ eclient_oserver_kex_strict + ]}, + {tunnel_distro_server, [], [tunnel_in_erlclient_openssh_server, + tunnel_out_erlclient_openssh_server]}, + {erlang_server, [], [{group, tunnel_distro_client}, + erlang_server_openssh_client_renegotiate, ++ eserver_oclient_kex_strict, + exec_with_io_in_sshc, + exec_direct_with_io_in_sshc + ] +@@ -87,16 +92,15 @@ + tunnel_out_non_erlclient_erlserver]} + ]. + +-init_per_suite(Config) -> ++init_per_suite(Config0) -> + ?CHECK_CRYPTO( +- case gen_tcp:connect("localhost", ?SSH_DEFAULT_PORT, []) of ++ case gen_tcp:connect("localhost", ?SSH_DEFAULT_PORT, [{active, false}]) of + {error,econnrefused} -> + {skip,"No openssh daemon (econnrefused)"}; +- _ -> ++ {ok, Sock} -> + ssh_test_lib:openssh_sanity_check( +- [{ptty_supported, ssh_test_lib:ptty_supported()} +- | Config] +- ) ++ [{ptty_supported, ssh_test_lib:ptty_supported()}, ++ {kex_strict, check_kex_strict(Sock)}| Config0]) + end + ). + +@@ -142,6 +146,25 @@ + %% Test Cases -------------------------------------------------------- + %%-------------------------------------------------------------------- + erlang_shell_client_openssh_server(Config) when is_list(Config) -> ++ eclient_oserver_helper(Config). ++ ++eclient_oserver_kex_strict(Config) when is_list(Config)-> ++ case proplists:get_value(kex_strict, Config) of ++ true -> ++ {ok, HandlerPid} = ssh_test_lib:add_report_handler(), ++ #{level := Level} = logger:get_primary_config(), ++ logger:set_primary_config(level, notice), ++ Result = eclient_oserver_helper(Config), ++ {ok, Reports} = ssh_test_lib:get_reports(HandlerPid), ++ ct:pal("Reports = ~p", [Reports]), ++ true = ssh_test_lib:kex_strict_negotiated(client, Reports), ++ logger:set_primary_config(Level), ++ Result; ++ _ -> ++ {skip, "KEX strict not support by local OpenSSH"} ++ end. ++ ++eclient_oserver_helper(Config) -> + process_flag(trap_exit, true), + IO = ssh_test_lib:start_io_server(), + Prev = lists:usort(supervisor:which_children(sshc_sup)), +@@ -166,7 +189,6 @@ + false + end) + end. +- + %%-------------------------------------------------------------------- + %% Test that the server could redirect stdin and stdout from/to an + %% OpensSSH client when handling an exec request +@@ -231,6 +253,25 @@ + %%-------------------------------------------------------------------- + %% Test that the Erlang/OTP server can renegotiate with openSSH + erlang_server_openssh_client_renegotiate(Config) -> ++ eserver_oclient_renegotiate_helper(Config). ++ ++eserver_oclient_kex_strict(Config) -> ++ case proplists:get_value(kex_strict, Config) of ++ true -> ++ {ok, HandlerPid} = ssh_test_lib:add_report_handler(), ++ #{level := Level} = logger:get_primary_config(), ++ logger:set_primary_config(level, notice), ++ Result = eserver_oclient_renegotiate_helper(Config), ++ {ok, Reports} = ssh_test_lib:get_reports(HandlerPid), ++ ct:log("Reports = ~p", [Reports]), ++ true = ssh_test_lib:kex_strict_negotiated(server, Reports), ++ logger:set_primary_config(Level), ++ Result; ++ _ -> ++ {skip, "KEX strict not support by local OpenSSH"} ++ end. ++ ++eserver_oclient_renegotiate_helper(Config) -> + _PubKeyAlg = ssh_rsa, + SystemDir = proplists:get_value(data_dir, Config), + PrivDir = proplists:get_value(priv_dir, Config), +@@ -255,9 +296,9 @@ + + OpenSsh = ssh_test_lib:open_port({spawn, Cmd++" < "++DataFile}), + +- Expect = fun({data,R}) -> ++ Expect = fun({data,R}) -> + try +- NonAlphaChars = [C || C<-lists:seq(1,255), ++ NonAlphaChars = [C || C<-lists:seq(1,255), + not lists:member(C,lists:seq($a,$z)), + not lists:member(C,lists:seq($A,$Z)) + ], +@@ -275,15 +316,14 @@ + (_) -> + false + end, +- +- try +- ssh_test_lib:rcv_expected(Expect, OpenSsh, ?TIMEOUT) ++ try ++ ssh_test_lib:rcv_expected(Expect, OpenSsh, ?TIMEOUT) + of +- _ -> +- %% Unfortunately we can't check that there has been a renegotiation, just trust OpenSSH. +- ssh:stop_daemon(Pid) ++ _ -> ++ %% Unfortunately we can't check that there has been a renegotiation, just trust OpenSSH. ++ ssh:stop_daemon(Pid) + catch +- throw:{skip,R} -> {skip,R} ++ throw:{skip,R} -> {skip,R} + end. + + %%-------------------------------------------------------------------- +@@ -569,3 +609,17 @@ + "---- The function no_forwarding() returns ~p", + [Cmnd,TheText, FailRegExp, Result]), + Result. ++ ++check_kex_strict(Sock) -> ++ %% Send some version, in order to receive KEXINIT from server ++ ok = gen_tcp:send(Sock, "SSH-2.0-OpenSSH_9.5\r\n"), ++ ct:sleep(100), ++ {ok, Packet} = gen_tcp:recv(Sock, 0), ++ case string:find(Packet, ?kex_strict_s) of ++ nomatch -> ++ ct:log("KEX strict NOT supported by local OpenSSH"), ++ false; ++ _ -> ++ ct:log("KEX strict supported by local OpenSSH"), ++ true ++ end. +--- a/lib/ssh/test/ssh_trpt_test_lib.erl ++++ b/lib/ssh/test/ssh_trpt_test_lib.erl +@@ -73,7 +73,7 @@ + exec(Op, S0=#s{}) -> + S1 = init_op_traces(Op, S0), + try seqnum_trace( +- op(Op, S1)) ++ op(Op, S1), S1) + of + S = #s{} -> + case proplists:get_value(silent,S#s.opts) of +@@ -331,12 +331,20 @@ + {Msg, _Bytes, _C0} = ssh_transport:key_exchange_init_msg(S0#s.ssh), + send(S0, Msg); + ++send(S0, ssh_msg_ignore) -> ++ Msg = #ssh_msg_ignore{data = "unexpected_ignore_message"}, ++ send(S0, Msg); ++ ++send(S0, ssh_msg_unknown) -> ++ Msg = binary:encode_hex(<<"0000000C060900000000000000000000">>), ++ send(S0, Msg); ++ + send(S0=#s{alg_neg={undefined,PeerMsg}}, Msg=#ssh_msg_kexinit{}) -> + S1 = opt(print_messages, S0, + fun(X) when X==true;X==detail -> {"Send~n~s~n",[format_msg(Msg)]} end), + S2 = case PeerMsg of + #ssh_msg_kexinit{} -> +- try ssh_transport:handle_kexinit_msg(PeerMsg, Msg, S1#s.ssh) of ++ try ssh_transport:handle_kexinit_msg(PeerMsg, Msg, S1#s.ssh, init) of + {ok,Cx} when ?role(S1) == server -> + S1#s{alg = Cx#ssh.algorithms}; + {ok,_NextKexMsgBin,Cx} when ?role(S1) == client -> +@@ -358,7 +366,7 @@ + send(S0, ssh_msg_kexdh_init) when ?role(S0) == client -> + {OwnMsg, PeerMsg} = S0#s.alg_neg, + {ok, NextKexMsgBin, C} = +- try ssh_transport:handle_kexinit_msg(PeerMsg, OwnMsg, S0#s.ssh) ++ try ssh_transport:handle_kexinit_msg(PeerMsg, OwnMsg, S0#s.ssh, init) + catch + Class:Exc -> + fail("Algorithm negotiation failed!", +@@ -441,7 +449,7 @@ + fail("2 kexint received!!", S); + + {OwnMsg, _} -> +- try ssh_transport:handle_kexinit_msg(PeerMsg, OwnMsg, S#s.ssh) of ++ try ssh_transport:handle_kexinit_msg(PeerMsg, OwnMsg, S#s.ssh, init) of + {ok,C} when ?role(S) == server -> + S#s{alg_neg = {OwnMsg, PeerMsg}, + alg = C#ssh.algorithms, +@@ -725,23 +733,23 @@ + fun(true) -> {"~s ~p",[Class,Term]} end) + ). + +-seqnum_trace(S) -> ++seqnum_trace(S, S0) -> + opt(print_seqnums, S, +- fun(true) when S#s.ssh#ssh.send_sequence =/= S#s.ssh#ssh.send_sequence, +- S#s.ssh#ssh.recv_sequence =/= S#s.ssh#ssh.recv_sequence -> ++ fun(true) when S0#s.ssh#ssh.send_sequence =/= S#s.ssh#ssh.send_sequence, ++ S0#s.ssh#ssh.recv_sequence =/= S#s.ssh#ssh.recv_sequence -> + {"~p seq num: send ~p->~p, recv ~p->~p~n", + [?role(S), +- S#s.ssh#ssh.send_sequence, S#s.ssh#ssh.send_sequence, +- S#s.ssh#ssh.recv_sequence, S#s.ssh#ssh.recv_sequence ++ S0#s.ssh#ssh.send_sequence, S#s.ssh#ssh.send_sequence, ++ S0#s.ssh#ssh.recv_sequence, S#s.ssh#ssh.recv_sequence + ]}; +- (true) when S#s.ssh#ssh.send_sequence =/= S#s.ssh#ssh.send_sequence -> ++ (true) when S0#s.ssh#ssh.send_sequence =/= S#s.ssh#ssh.send_sequence -> + {"~p seq num: send ~p->~p~n", + [?role(S), +- S#s.ssh#ssh.send_sequence, S#s.ssh#ssh.send_sequence]}; +- (true) when S#s.ssh#ssh.recv_sequence =/= S#s.ssh#ssh.recv_sequence -> ++ S0#s.ssh#ssh.send_sequence, S#s.ssh#ssh.send_sequence]}; ++ (true) when S0#s.ssh#ssh.recv_sequence =/= S#s.ssh#ssh.recv_sequence -> + {"~p seq num: recv ~p->~p~n", + [?role(S), +- S#s.ssh#ssh.recv_sequence, S#s.ssh#ssh.recv_sequence]} ++ S0#s.ssh#ssh.recv_sequence, S#s.ssh#ssh.recv_sequence]} + end). + + print_traces(S) when S#s.prints == [] -> S; diff -Nru erlang-25.2.3+dfsg/debian/patches/ssh-custom_kexinit-test-added.patch erlang-25.2.3+dfsg/debian/patches/ssh-custom_kexinit-test-added.patch --- erlang-25.2.3+dfsg/debian/patches/ssh-custom_kexinit-test-added.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-25.2.3+dfsg/debian/patches/ssh-custom_kexinit-test-added.patch 2025-04-20 05:09:59.000000000 +0000 @@ -0,0 +1,136 @@ +From: Jakub Witczak +Date: Mon, 24 Mar 2025 11:31:39 +0100 +Subject: ssh: custom_kexinit test added + The test uses big KEX init packet which causes large memory + consumption for Erlang prior to 25.3.2.19 +Origin: https://github.com/erlang/otp/commit/5ee26eb412a76ba1c6afdf4524b62939a48d1bce +Bug-Debian: https://bugs.debian.org/1101713 +Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2025-30211 + +--- a/lib/ssh/test/ssh_protocol_SUITE.erl ++++ b/lib/ssh/test/ssh_protocol_SUITE.erl +@@ -72,6 +72,7 @@ + modify_rm/1, + no_common_alg_client_disconnects/1, + no_common_alg_server_disconnects/1, ++ custom_kexinit/1, + no_ext_info_s1/1, + no_ext_info_s2/1, + packet_length_too_large/1, +@@ -132,7 +133,8 @@ + {field_size_error, [], [service_name_length_too_large, + service_name_length_too_short]}, + +- {kex, [], [no_common_alg_server_disconnects, ++ {kex, [], [custom_kexinit, ++ no_common_alg_server_disconnects, + no_common_alg_client_disconnects, + gex_client_init_option_groups, + gex_server_gex_limit, +@@ -173,7 +175,7 @@ + end_per_suite(Config) -> + stop_apps(Config). + +-init_per_testcase(no_common_alg_server_disconnects, Config) -> ++init_per_testcase(Tc, Config) when Tc == no_common_alg_server_disconnects; Tc == custom_kexinit -> + start_std_daemon(Config, [{preferred_algorithms,[{public_key,['ssh-rsa']}, + {cipher,?DEFAULT_CIPHERS} + ]}]); +@@ -219,7 +221,7 @@ + init_per_testcase(_TestCase, Config) -> + check_std_daemon_works(Config, ?LINE). + +-end_per_testcase(no_common_alg_server_disconnects, Config) -> ++end_per_testcase(Tc, Config) when Tc == no_common_alg_server_disconnects; Tc == custom_kexinit -> + stop_std_daemon(Config); + end_per_testcase(kex_strict_negotiated, Config) -> + Config; +@@ -380,6 +382,88 @@ + ] + ). + ++custom_kexinit(Config) -> ++ %% 16#C0 value causes unicode:characters_to_list to return a big error value ++ Trash = lists:duplicate(260_000, 16#C0), ++ FunnyAlg = "curve25519-sha256", ++ KexInit = ++ #ssh_msg_kexinit{cookie = <<"Ã/Ï!9zñKá:ñÀv¿JÜ">>, ++ kex_algorithms = ++ [FunnyAlg ++ Trash], ++ server_host_key_algorithms = ["ssh-rsa"], ++ encryption_algorithms_client_to_server = ++ ["aes256-ctr","aes192-ctr","aes128-ctr","aes128-cbc","3des-cbc"], ++ encryption_algorithms_server_to_client = ++ ["aes256-ctr","aes192-ctr","aes128-ctr","aes128-cbc","3des-cbc"], ++ mac_algorithms_client_to_server = ++ ["hmac-sha2-512-etm@openssh.com","hmac-sha2-256-etm@openssh.com", ++ "hmac-sha2-512","hmac-sha2-256","hmac-sha1-etm@openssh.com","hmac-sha1"], ++ mac_algorithms_server_to_client = ++ ["hmac-sha2-512-etm@openssh.com","hmac-sha2-256-etm@openssh.com", ++ "hmac-sha2-512","hmac-sha2-256","hmac-sha1-etm@openssh.com","hmac-sha1"], ++ compression_algorithms_client_to_server = ["none","zlib@openssh.com","zlib"], ++ compression_algorithms_server_to_client = ["none","zlib@openssh.com","zlib"], ++ languages_client_to_server = [], ++ languages_server_to_client = [], ++ first_kex_packet_follows = false, ++ reserved = 0 ++ }, ++ PacketFun = ++ fun(Msg, Ssh) -> ++ BinMsg = custom_encode(Msg), ++ ssh_transport:pack(BinMsg, Ssh, 0) ++ end, ++ {ok,_} = ++ ssh_trpt_test_lib:exec( ++ [{set_options, [print_ops, {print_messages,detail}]}, ++ {connect, ++ server_host(Config),server_port(Config), ++ [{silently_accept_hosts, true}, ++ {user_dir, user_dir(Config)}, ++ {user_interaction, false}, ++ {preferred_algorithms,[{public_key,['ssh-rsa']}, ++ {cipher,?DEFAULT_CIPHERS} ++ ]} ++ ]}, ++ receive_hello, ++ {send, hello}, ++ {match, #ssh_msg_kexinit{_='_'}, receive_msg}, ++ {send, {special, KexInit, PacketFun}}, % with server unsupported 'ssh-dss' ! ++ {match, disconnect(), receive_msg} ++ ] ++ ). ++ ++custom_encode(#ssh_msg_kexinit{ ++ cookie = Cookie, ++ kex_algorithms = KeyAlgs, ++ server_host_key_algorithms = HostKeyAlgs, ++ encryption_algorithms_client_to_server = EncAlgC2S, ++ encryption_algorithms_server_to_client = EncAlgS2C, ++ mac_algorithms_client_to_server = MacAlgC2S, ++ mac_algorithms_server_to_client = MacAlgS2C, ++ compression_algorithms_client_to_server = CompAlgS2C, ++ compression_algorithms_server_to_client = CompAlgC2S, ++ languages_client_to_server = LangC2S, ++ languages_server_to_client = LangS2C, ++ first_kex_packet_follows = Bool, ++ reserved = Reserved ++ }) -> ++ KeyAlgsBin0 = <>, ++ <> = KeyAlgsBin0, ++ KeyAlgsBin = <>, ++ <>. ++ + %%-------------------------------------------------------------------- + %%% Algo negotiation fail. This should result in a ssh_msg_disconnect + %%% being sent from the client. diff -Nru erlang-25.2.3+dfsg/debian/patches/ssh-early-RCE-fix.patch erlang-25.2.3+dfsg/debian/patches/ssh-early-RCE-fix.patch --- erlang-25.2.3+dfsg/debian/patches/ssh-early-RCE-fix.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-25.2.3+dfsg/debian/patches/ssh-early-RCE-fix.patch 2025-04-20 05:09:59.000000000 +0000 @@ -0,0 +1,212 @@ +From: Jakub Witczak +Date: Mon, 14 Apr 2025 16:33:21 +0200 +Subject: ssh: early RCE fix + The patch fixes remote code execution (RCE) by an unauthenticated user. + - disconnect when connection protocol message arrives + - when user is not authenticated for connection + - see RFC4252 sec.6 +Origin: https://github.com/erlang/otp/commit/0fcd9c56524b28615e8ece65fc0c3f66ef6e4c12 +Bug-Debian: https://bugs.debian.org/1103442 +Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2025-32433 + +--- a/lib/ssh/src/ssh_connection.erl ++++ b/lib/ssh/src/ssh_connection.erl +@@ -26,6 +26,8 @@ + + -module(ssh_connection). + ++-include_lib("kernel/include/logger.hrl"). ++ + -include("ssh.hrl"). + -include("ssh_connect.hrl"). + -include("ssh_transport.hrl"). +@@ -468,6 +470,25 @@ + %%% Replies {Reply, UpdatedConnection} + %%% + ++handle_msg(#ssh_msg_disconnect{code = Code, description = Description}, Connection, _, _SSH) -> ++ {disconnect, {Code, Description}, handle_stop(Connection)}; ++ ++handle_msg(Msg, Connection, server, Ssh = #ssh{authenticated = false}) -> ++ %% See RFC4252 6. ++ %% Message numbers of 80 and higher are reserved for protocols running ++ %% after this authentication protocol, so receiving one of them before ++ %% authentication is complete is an error, to which the server MUST ++ %% 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}]) ++ end, ++ ?LOG_DEBUG(MsgFun, [Msg]), ++ {disconnect, {?SSH_DISCONNECT_PROTOCOL_ERROR, "Connection refused"}, handle_stop(Connection)}; ++ + handle_msg(#ssh_msg_channel_open_confirmation{recipient_channel = ChannelId, + sender_channel = RemoteId, + initial_window_size = WindowSz, +@@ -966,12 +987,7 @@ + #connection{requests = [{_, From, Fun} | Rest]} = Connection0, _, _SSH) -> + Connection = Fun({success,Data}, Connection0), + {[{channel_request_reply, From, {success, Data}}], +- Connection#connection{requests = Rest}}; +- +-handle_msg(#ssh_msg_disconnect{code = Code, +- description = Description}, +- Connection, _, _SSH) -> +- {disconnect, {Code, Description}, handle_stop(Connection)}. ++ Connection#connection{requests = Rest}}. + + + %%%---------------------------------------------------------------- +--- a/lib/ssh/test/ssh_protocol_SUITE.erl ++++ b/lib/ssh/test/ssh_protocol_SUITE.erl +@@ -73,6 +73,7 @@ + no_common_alg_client_disconnects/1, + no_common_alg_server_disconnects/1, + custom_kexinit/1, ++ early_rce/1, + no_ext_info_s1/1, + no_ext_info_s2/1, + packet_length_too_large/1, +@@ -111,6 +112,7 @@ + all() -> + [{group,tool_tests}, + client_info_line, ++ early_rce, + {group,kex}, + {group,service_requests}, + {group,authentication}, +@@ -129,10 +131,8 @@ + ]}, + {packet_size_error, [], [packet_length_too_large, + packet_length_too_short]}, +- + {field_size_error, [], [service_name_length_too_large, + service_name_length_too_short]}, +- + {kex, [], [custom_kexinit, + no_common_alg_server_disconnects, + no_common_alg_client_disconnects, +@@ -175,7 +175,8 @@ + end_per_suite(Config) -> + stop_apps(Config). + +-init_per_testcase(Tc, Config) when Tc == no_common_alg_server_disconnects; Tc == custom_kexinit -> ++init_per_testcase(Tc, Config) when Tc == no_common_alg_server_disconnects; ++ Tc == custom_kexinit -> + start_std_daemon(Config, [{preferred_algorithms,[{public_key,['ssh-rsa']}, + {cipher,?DEFAULT_CIPHERS} + ]}]); +@@ -221,7 +222,8 @@ + init_per_testcase(_TestCase, Config) -> + check_std_daemon_works(Config, ?LINE). + +-end_per_testcase(Tc, Config) when Tc == no_common_alg_server_disconnects; Tc == custom_kexinit -> ++end_per_testcase(Tc, Config) when Tc == no_common_alg_server_disconnects; ++ Tc == custom_kexinit -> + stop_std_daemon(Config); + end_per_testcase(kex_strict_negotiated, Config) -> + Config; +@@ -382,6 +384,44 @@ + ] + ). + ++early_rce(Config) -> ++ {ok,InitialState} = ++ ssh_trpt_test_lib:exec([{set_options, [print_ops, print_seqnums, print_messages]}]), ++ TypeOpen = "session", ++ ChannelId = 0, ++ WinSz = 425984, ++ PktSz = 65536, ++ DataOpen = <<>>, ++ SshMsgChannelOpen = ssh_connection:channel_open_msg(TypeOpen, ChannelId, WinSz, PktSz, DataOpen), ++ ++ Id = 0, ++ TypeReq = "exec", ++ WantReply = true, ++ DataReq = <>)>>, ++ SshMsgChannelRequest = ++ ssh_connection:channel_request_msg(Id, TypeReq, WantReply, DataReq), ++ {ok,AfterKexState} = ++ ssh_trpt_test_lib:exec( ++ [{connect, ++ server_host(Config),server_port(Config), ++ [{preferred_algorithms,[{kex,[?DEFAULT_KEX]}, ++ {cipher,?DEFAULT_CIPHERS} ++ ]}, ++ {silently_accept_hosts, true}, ++ {recv_ext_info, false}, ++ {user_dir, user_dir(Config)}, ++ {user_interaction, false} ++ | proplists:get_value(extra_options,Config,[])]}, ++ receive_hello, ++ {send, hello}, ++ {send, ssh_msg_kexinit}, ++ {match, #ssh_msg_kexinit{_='_'}, receive_msg}, ++ {send, SshMsgChannelOpen}, ++ {send, SshMsgChannelRequest}, ++ {match, disconnect(), receive_msg} ++ ], InitialState), ++ ok. ++ + custom_kexinit(Config) -> + %% 16#C0 value causes unicode:characters_to_list to return a big error value + Trash = lists:duplicate(260_000, 16#C0), +@@ -408,11 +448,6 @@ + first_kex_packet_follows = false, + reserved = 0 + }, +- PacketFun = +- fun(Msg, Ssh) -> +- BinMsg = custom_encode(Msg), +- ssh_transport:pack(BinMsg, Ssh, 0) +- end, + {ok,_} = + ssh_trpt_test_lib:exec( + [{set_options, [print_ops, {print_messages,detail}]}, +@@ -428,42 +463,11 @@ + receive_hello, + {send, hello}, + {match, #ssh_msg_kexinit{_='_'}, receive_msg}, +- {send, {special, KexInit, PacketFun}}, % with server unsupported 'ssh-dss' ! ++ {send, KexInit}, % with server unsupported 'ssh-dss' ! + {match, disconnect(), receive_msg} + ] + ). + +-custom_encode(#ssh_msg_kexinit{ +- cookie = Cookie, +- kex_algorithms = KeyAlgs, +- server_host_key_algorithms = HostKeyAlgs, +- encryption_algorithms_client_to_server = EncAlgC2S, +- encryption_algorithms_server_to_client = EncAlgS2C, +- mac_algorithms_client_to_server = MacAlgC2S, +- mac_algorithms_server_to_client = MacAlgS2C, +- compression_algorithms_client_to_server = CompAlgS2C, +- compression_algorithms_server_to_client = CompAlgC2S, +- languages_client_to_server = LangC2S, +- languages_server_to_client = LangS2C, +- first_kex_packet_follows = Bool, +- reserved = Reserved +- }) -> +- KeyAlgsBin0 = <>, +- <> = KeyAlgsBin0, +- KeyAlgsBin = <>, +- <>. +- + %%-------------------------------------------------------------------- + %%% Algo negotiation fail. This should result in a ssh_msg_disconnect + %%% being sent from the client. diff -Nru erlang-25.2.3+dfsg/debian/patches/ssh-ignore-too-long-names.patch erlang-25.2.3+dfsg/debian/patches/ssh-ignore-too-long-names.patch --- erlang-25.2.3+dfsg/debian/patches/ssh-ignore-too-long-names.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-25.2.3+dfsg/debian/patches/ssh-ignore-too-long-names.patch 2025-04-20 05:09:59.000000000 +0000 @@ -0,0 +1,53 @@ +From: Jakub Witczak +Date: Fri, 21 Mar 2025 12:17:07 +0100 +Subject: ssh: ignore too long names + The patch makes Erlang SSH application ignore long + algorithm names in order to fix denial of service because + of high memory consumption. +Origin: https://github.com/erlang/otp/commit/655e20a49ef80431e86ffb6c7f366d01fd4b64c3 +Bug-Debian: https://bugs.debian.org/1101713 +Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2025-30211 + +--- a/lib/ssh/src/ssh_message.erl ++++ b/lib/ssh/src/ssh_message.erl +@@ -24,6 +24,7 @@ + -module(ssh_message). + + -include_lib("public_key/include/public_key.hrl"). ++-include_lib("kernel/include/logger.hrl"). + + -include("ssh.hrl"). + -include("ssh_connect.hrl"). +@@ -42,6 +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). + + ucl(B) -> + try unicode:characters_to_list(B) of +@@ -820,8 +822,22 @@ + X = 0, + list_to_tuple(lists:reverse([X, erl_boolean(Bool) | Acc])); + decode_kex_init(<>, Acc, N) -> +- Names = string:tokens(?unicode_list(Data), ","), +- decode_kex_init(Rest, [Names | Acc], N -1). ++ 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", []), ++ 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). + + + %%%================================================================ diff -Nru erlang-25.2.3+dfsg/debian/patches/ssh-reduce-log-processing-for-plain-connections.patch erlang-25.2.3+dfsg/debian/patches/ssh-reduce-log-processing-for-plain-connections.patch --- erlang-25.2.3+dfsg/debian/patches/ssh-reduce-log-processing-for-plain-connections.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-25.2.3+dfsg/debian/patches/ssh-reduce-log-processing-for-plain-connections.patch 2025-04-20 05:09:59.000000000 +0000 @@ -0,0 +1,133 @@ +From: Jakub Witczak +Date: Thu, 13 Mar 2025 13:38:29 +0100 +Subject: ssh: reduce log processing for plain connections + - avoid unnecessary data processing +Origin: https://github.com/erlang/otp/commit/df3aad2c5570847895562ff96a725190571f028c +Bug-Debian: https://bugs.debian.org/1101713 +Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2025-30211 + +--- a/lib/ssh/src/ssh.hrl ++++ b/lib/ssh/src/ssh.hrl +@@ -561,5 +561,11 @@ + -define(CIRC_BUF_IN_ONCE(VALUE), + ((fun(V) -> ?CIRC_BUF_IN(V), V end)(VALUE)) + ). +- ++ ++-define(SELECT_MSG(__Fun), ++ (fun() -> ++ #{level := __Level} = logger:get_primary_config(), ++ __Fun(__Level) ++ end)()). ++ + -endif. % SSH_HRL defined +--- a/lib/ssh/src/ssh_acceptor.erl ++++ b/lib/ssh/src/ssh_acceptor.erl +@@ -206,39 +206,63 @@ + handle_error(Reason, ToAddress, ToPort, FromAddress, FromPort) -> + case Reason of + {max_sessions, MaxSessions} -> +- error_logger:info_report( +- lists:concat(["Ssh login attempt to ",ssh_lib:format_address_port(ToAddress,ToPort), +- " from ",ssh_lib:format_address_port(FromAddress,FromPort), +- " denied due to option max_sessions limits to ", +- MaxSessions, " sessions." +- ]) +- ); +- ++ MsgFun = ++ fun(debug) -> ++ lists:concat(["Ssh login attempt to ", ++ ssh_lib:format_address_port(ToAddress,ToPort), ++ " from ", ++ ssh_lib:format_address_port(FromAddress,FromPort), ++ " denied due to option max_sessions limits to ", ++ MaxSessions, " sessions."]); ++ (_) -> ++ ["Ssh login attempt denied max_session limits"] ++ end, ++ error_logger:info_report(?SELECT_MSG(MsgFun)); + Limit when Limit==enfile ; Limit==emfile -> + %% Out of sockets... +- error_logger:info_report([atom_to_list(Limit),": out of accept sockets on ", +- ssh_lib:format_address_port(ToAddress, ToPort), +- " - retrying"]), ++ MsgFun = ++ fun(debug) -> ++ [atom_to_list(Limit),": out of accept sockets on ", ++ ssh_lib:format_address_port(ToAddress, ToPort), ++ " - retrying"]; ++ (_) -> ++ ["Out of accept sockets on - retrying"] ++ end, ++ error_logger:info_report(?SELECT_MSG(MsgFun)), + timer:sleep(?SLEEP_TIME); +- + closed -> +- error_logger:info_report(["The ssh accept socket on ",ssh_lib:format_address_port(ToAddress,ToPort), +- "was closed by a third party."] +- ); +- ++ MsgFun = ++ fun(debug) -> ++ ["The ssh accept socket on ", ssh_lib:format_address_port(ToAddress,ToPort), ++ "was closed by a third party."]; ++ (_) -> ++ ["The ssh accept socket on was closed by a third party"] ++ end, ++ error_logger:info_report(?SELECT_MSG(MsgFun)); + timeout -> + ok; +- + Error when is_list(Error) -> + ok; + Error when FromAddress=/=undefined, + FromPort=/=undefined -> +- error_logger:info_report(["Accept failed on ",ssh_lib:format_address_port(ToAddress,ToPort), +- " for connect from ",ssh_lib:format_address_port(FromAddress,FromPort), +- io_lib:format(": ~p", [Error])]); ++ MsgFun = ++ fun(debug) -> ++ ["Accept failed on ",ssh_lib:format_address_port(ToAddress,ToPort), ++ " for connect from ",ssh_lib:format_address_port(FromAddress,FromPort), ++ io_lib:format(": ~p", [Error])]; ++ (_) -> ++ [io_lib:format("Accept failed on for connection: ~p", [Error])] ++ end, ++ error_logger:info_report(?SELECT_MSG(MsgFun)); + Error -> +- error_logger:info_report(["Accept failed on ",ssh_lib:format_address_port(ToAddress,ToPort), +- io_lib:format(": ~p", [Error])]) ++ MsgFun = ++ fun(debug) -> ++ ["Accept failed on ",ssh_lib:format_address_port(ToAddress,ToPort), ++ io_lib:format(": ~p", [Error])]; ++ (_) -> ++ [io_lib:format("Accept failed on for connection: ~p", [Error])] ++ end, ++ error_logger:info_report(?SELECT_MSG(MsgFun)) + end. + + %%%---------------------------------------------------------------- +--- a/lib/ssh/src/ssh_connection_handler.erl ++++ b/lib/ssh/src/ssh_connection_handler.erl +@@ -648,11 +648,15 @@ + + %%% timeout after tcp:connect but then nothing arrives + handle_event(state_timeout, no_hello_received, {hello,_Role}=StateName, D0 = #data{ssh_params = Ssh0}) -> +- Time = ?GET_OPT(hello_timeout, Ssh0#ssh.opts), ++ MsgFun = ++ fun (debug) -> ++ Time = ?GET_OPT(hello_timeout, Ssh0#ssh.opts), ++ lists:concat(["No HELLO received within ",ssh_lib:format_time_ms(Time)]); ++ (_) -> ++ ["No HELLO received within hello_timeout"] ++ end, + {Shutdown, D} = +- ?send_disconnect(?SSH_DISCONNECT_PROTOCOL_ERROR, +- lists:concat(["No HELLO received within ",ssh_lib:format_time_ms(Time)]), +- StateName, D0), ++ ?send_disconnect(?SSH_DISCONNECT_PROTOCOL_ERROR, ?SELECT_MSG(MsgFun), StateName, D0), + {stop, Shutdown, D}; + + diff -Nru erlang-25.2.3+dfsg/debian/patches/ssh-sftp-reject-packets-exceeding-limit.patch erlang-25.2.3+dfsg/debian/patches/ssh-sftp-reject-packets-exceeding-limit.patch --- erlang-25.2.3+dfsg/debian/patches/ssh-sftp-reject-packets-exceeding-limit.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-25.2.3+dfsg/debian/patches/ssh-sftp-reject-packets-exceeding-limit.patch 2025-04-20 05:09:59.000000000 +0000 @@ -0,0 +1,83 @@ +From: Jakub Witczak +Date: Thu, 6 Feb 2025 19:00:44 +0100 +Subject: ssh: sftp reject packets exceeding limit +Origin: https://github.com/erlang/otp/commit/0ed2573cbd55c92e9125c9dc70fa1ca7fed82872 +Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2025-26618 + +--- a/lib/ssh/src/ssh_sftpd.erl ++++ b/lib/ssh/src/ssh_sftpd.erl +@@ -27,7 +27,7 @@ + -behaviour(ssh_server_channel). + + -include_lib("kernel/include/file.hrl"). +- ++-include_lib("kernel/include/logger.hrl"). + -include("ssh.hrl"). + -include("ssh_xfer.hrl"). + -include("ssh_connect.hrl"). %% For ?DEFAULT_PACKET_SIZE and ?DEFAULT_WINDOW_SIZE +@@ -128,9 +128,8 @@ + %% Description: Handles channel messages + %%-------------------------------------------------------------------- + handle_ssh_msg({ssh_cm, _ConnectionManager, +- {data, _ChannelId, Type, Data}}, State) -> +- State1 = handle_data(Type, Data, State), +- {ok, State1}; ++ {data, ChannelId, Type, Data}}, State) -> ++ handle_data(Type, ChannelId, Data, State); + + handle_ssh_msg({ssh_cm, _, {eof, ChannelId}}, State) -> + {stop, ChannelId, State}; +@@ -187,24 +186,42 @@ + %%-------------------------------------------------------------------- + %%% Internal functions + %%-------------------------------------------------------------------- +-handle_data(0, <>, ++handle_data(0, ChannelId, <>, + State = #state{pending = <<>>}) -> + <> = Msg, + NewState = handle_op(Op, ReqId, Data, State), + case Rest of + <<>> -> +- NewState; ++ {ok, NewState}; + _ -> +- handle_data(0, Rest, NewState) ++ handle_data(0, ChannelId, Rest, NewState) + end; +- +-handle_data(0, Data, State = #state{pending = <<>>}) -> +- State#state{pending = Data}; +- +-handle_data(Type, Data, State = #state{pending = Pending}) -> +- handle_data(Type, <>, +- State#state{pending = <<>>}). +- ++handle_data(0, _ChannelId, Data, State = #state{pending = <<>>}) -> ++ {ok, State#state{pending = Data}}; ++handle_data(Type, ChannelId, Data0, State = #state{pending = Pending}) -> ++ Data = <>, ++ Size = byte_size(Data), ++ case Size > ?SSH_MAX_PACKET_SIZE of ++ true -> ++ ReportFun = ++ fun([S]) -> ++ Report = ++ #{label => {error_logger, error_report}, ++ report => ++ io_lib:format("SFTP packet size (~B) exceeds the limit!", ++ [S])}, ++ Meta = ++ #{error_logger => ++ #{tag => error_report,type => std_error}, ++ report_cb => fun(#{report := Msg}) -> {Msg, []} end}, ++ {Report, Meta} ++ end, ++ ?LOG_ERROR(ReportFun, [Size]), ++ {stop, ChannelId, State}; ++ _ -> ++ handle_data(Type, ChannelId, Data, State#state{pending = <<>>}) ++ end. ++ + 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 -Nru erlang-25.2.3+dfsg/debian/patches/ssh-use-chars_limit-for-bad-packets-error-messages.patch erlang-25.2.3+dfsg/debian/patches/ssh-use-chars_limit-for-bad-packets-error-messages.patch --- erlang-25.2.3+dfsg/debian/patches/ssh-use-chars_limit-for-bad-packets-error-messages.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-25.2.3+dfsg/debian/patches/ssh-use-chars_limit-for-bad-packets-error-messages.patch 2025-04-20 05:09:59.000000000 +0000 @@ -0,0 +1,33 @@ +From: Jakub Witczak +Date: Fri, 21 Mar 2025 17:50:21 +0100 +Subject: ssh: use chars_limit for bad packets error messages + The patch limits the length of error messages sent in reply + to very long invalid packets. +Origin: https://github.com/erlang/otp/commit/d64d9fb0688092356a336e38a8717499113312a0 +Bug-Debian: https://bugs.debian.org/1101713 +Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2025-30211 + +--- a/lib/ssh/src/ssh_connection_handler.erl ++++ b/lib/ssh/src/ssh_connection_handler.erl +@@ -1158,8 +1158,8 @@ + MaxLogItemLen = ?GET_OPT(max_log_item_len,SshParams#ssh.opts), + {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,MaxLogItemLen]), ++ io_lib:format("Bad packet: Decrypted, but can't decode~n~p:~p~n~p", ++ [C,E,ST], [{chars_limit, MaxLogItemLen}]), + StateName, D1), + {stop, Shutdown, D} + end; +@@ -1193,8 +1193,8 @@ + MaxLogItemLen = ?GET_OPT(max_log_item_len,SshParams#ssh.opts), + {Shutdown, D} = + ?send_disconnect(?SSH_DISCONNECT_PROTOCOL_ERROR, +- io_lib:format("Bad packet: Couldn't decrypt~n~p:~p~n~P", +- [C,E,ST,MaxLogItemLen]), ++ io_lib:format("Bad packet: Couldn't decrypt~n~p:~p~n~p", ++ [C,E,ST], [{chars_limit, MaxLogItemLen}]), + StateName, D0), + {stop, Shutdown, D} + end;