From 642b2b51554e056ebcbb3d5c41bcabdc8b57fd1b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micha=C5=82=20W=C4=85sowski?= <michal@erlang.org>
Date: Sat, 7 Mar 2026 18:26:04 +0100
Subject: [PATCH 1/6] Disable zlib by default and limit size of decompressed
 data

---
 lib/ssh/src/ssh_connection_handler.erl |  7 +++
 lib/ssh/src/ssh_transport.erl          | 64 ++++++++++++++++++++++----
 2 files changed, 63 insertions(+), 8 deletions(-)

Index: otp-OTP-27.1.3/lib/ssh/src/ssh_connection_handler.erl
===================================================================
--- otp-OTP-27.1.3.orig/lib/ssh/src/ssh_connection_handler.erl
+++ otp-OTP-27.1.3/lib/ssh/src/ssh_connection_handler.erl
@@ -1230,6 +1230,13 @@ handle_event(info, {Proto, Sock, NewData
                                  io_lib:format("Bad packet: Size (~p bytes) exceeds max size",
                                                [PacketLen]),
                                  StateName, D0),
+            {stop, Shutdown, D};
+
+    {error, exceeds_max_decompressed_size} ->
+            {Shutdown, D} =
+                ?send_disconnect(?SSH_DISCONNECT_PROTOCOL_ERROR,
+                                 "Bad packet: Size after decompression exceeds max size",
+                                 StateName, D0),
             {stop, Shutdown, D}
     catch
 	Class:Reason0:Stacktrace ->
Index: otp-OTP-27.1.3/lib/ssh/src/ssh_transport.erl
===================================================================
--- otp-OTP-27.1.3.orig/lib/ssh/src/ssh_transport.erl
+++ otp-OTP-27.1.3/lib/ssh/src/ssh_transport.erl
@@ -193,6 +193,9 @@ default_algorithms1(public_key) ->
                                       'ssh-dss'
                                      ]);
 
+default_algorithms1(compression) ->
+    supported_algorithms(compression, same(['zlib']));
+
 default_algorithms1(Alg) ->
     supported_algorithms(Alg, []).
 
@@ -1433,8 +1436,12 @@ handle_packet_part(DecryptedPfx, Encrypt
     case unpack(pkt_type(CryptoAlg), mac_type(MacAlg),
                 DecryptedPfx, EncryptedBuffer, AEAD, TotalNeeded, Ssh0) of
         {ok, Payload, NextPacketBytes, Ssh1} ->
-            {Ssh, DecompressedPayload} = decompress(Ssh1, Payload),
-            {packet_decrypted, DecompressedPayload, NextPacketBytes, Ssh};
+            case decompress(Ssh1, Payload) of
+                {ok, Ssh, DecompressedPayload} ->
+                    {packet_decrypted, DecompressedPayload, NextPacketBytes, Ssh};
+                Other ->
+                    Other
+            end;
         Other ->
             Other
     end.
@@ -1950,15 +1957,56 @@ decompress_final(#ssh{decompress = 'zlib
     {ok, Ssh#ssh{decompress = none, decompress_ctx = undefined}}.
 
 decompress(#ssh{decompress = none} = Ssh, Data) ->
-    {Ssh, Data};
+    {ok, Ssh, Data};
 decompress(#ssh{decompress = zlib, decompress_ctx = Context} = Ssh, Data) ->
-    Decompressed = zlib:inflate(Context, Data),
-    {Ssh, list_to_binary(Decompressed)};
+    case safe_zlib_inflate(Context, Data) of
+        {ok, Decompressed} ->
+            {ok, Ssh, Decompressed};
+        Other ->
+            Other
+    end;
 decompress(#ssh{decompress = 'zlib@openssh.com', authenticated = false} = Ssh, Data) ->
-    {Ssh, Data};
+    {ok, Ssh, Data};
 decompress(#ssh{decompress = 'zlib@openssh.com', decompress_ctx = Context, authenticated = true} = Ssh, Data) ->
-    Decompressed = zlib:inflate(Context, Data),
-    {Ssh, list_to_binary(Decompressed)}.
+    case safe_zlib_inflate(Context, Data) of
+        {ok, Decompressed} ->
+            {ok, Ssh, Decompressed};
+        Other ->
+            Other
+    end.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% Safe decompression loop
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+safe_zlib_inflate(Context, Data) ->
+    safe_zlib_inflate_loop(Context, {0, []}, zlib:safeInflate(Context, Data)).
+
+safe_zlib_inflate_loop(Context, {AccLen0, AccData}, {Status, Chunk})
+  when Status == continue; Status == finished ->
+    ChunkLen = iolist_size(Chunk),
+    AccLen = AccLen0 + ChunkLen,
+    %% RFC 4253 section 6
+    %% Align with packets that don't use compression, we can process payloads with length
+    %% that required minimum padding.
+    %% From ?SSH_MAX_PACKET_SIZE subtract:
+    %% 1 byte for length of padding_length field
+    %% 4 bytes for minimum allowed length of padding
+    %% We don't subtract:
+    %% 4 bytes for packet_length field - not included in packet_length
+    %% x bytes for mac (size depends on type of used mac) - not included in packet_length
+    case AccLen > (?SSH_MAX_PACKET_SIZE - 5) of
+        true ->
+            {error, exceeds_max_decompressed_size};
+        false when Status == continue ->
+            Next = zlib:safeInflate(Context, []),
+            safe_zlib_inflate_loop(Context, {AccLen, [Chunk | AccData]}, Next);
+        false when Status == finished ->
+            Reversed = lists:reverse([Chunk | AccData]),
+            {ok, iolist_to_binary(Reversed)}
+    end;
+safe_zlib_inflate_loop(_Context, {_AccLen, _AccData}, {need_dictionary, Adler, _Chunk}) ->
+    erlang:error({need_dictionary, Adler}).
 
 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 %%
Index: otp-OTP-27.1.3/lib/ssh/test/ssh_basic_SUITE.erl
===================================================================
--- otp-OTP-27.1.3.orig/lib/ssh/test/ssh_basic_SUITE.erl
+++ otp-OTP-27.1.3/lib/ssh/test/ssh_basic_SUITE.erl
@@ -56,6 +56,7 @@
          double_close/1,
          exec/1,
          exec_compressed/1,
+         exec_compressed_post_auth_compression/1,
          exec_with_io_in/1,
          exec_with_io_out/1,
          host_equal/2,
@@ -153,7 +154,7 @@ groups() ->
                                            ]},
      
      {p_basic, [?PARALLEL], [send, peername_sockname,
-                             exec, exec_compressed, 
+                             exec, exec_compressed, exec_compressed_post_auth_compression,
                              exec_with_io_out, exec_with_io_in,
                              cli, cli_exit_normal, cli_exit_status,
                              idle_time_client, idle_time_server,
@@ -401,37 +402,42 @@ exec_with_io_in(Config) when is_list(Con
 %%--------------------------------------------------------------------
 %%% Test that compression option works
 exec_compressed(Config) when is_list(Config) ->
-    case ssh_test_lib:ssh_supports(zlib, compression) of
-	false ->
-	    {skip, "zlib compression is not supported"};
+    exec_compressed_helper(Config, 'zlib').
 
-	true ->
-	    process_flag(trap_exit, true),
-	    SystemDir = filename:join(proplists:get_value(priv_dir, Config), system),
-	    UserDir = proplists:get_value(priv_dir, Config), 
-
-	    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir},{user_dir, UserDir},
-						     {preferred_algorithms,[{compression, [zlib]}]},
-						     {failfun, fun ssh_test_lib:failfun/2}]),
-    
-	    ConnectionRef =
-		ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
-						  {user_dir, UserDir},
-						  {user_interaction, false}]),
-	    {ok, ChannelId} = ssh_connection:session_channel(ConnectionRef, infinity),
-	    success = ssh_connection:exec(ConnectionRef, ChannelId,
-					  "1+1.", infinity),
-	    Data = {ssh_cm, ConnectionRef, {data, ChannelId, 0, <<"2">>}},
-	    case ssh_test_lib:receive_exec_result(Data) of
-		expected ->
-		    ok;
-		Other ->
-		    ct:fail(Other)
-	    end,
-	    ssh_test_lib:receive_exec_end(ConnectionRef, ChannelId),
-	    ssh:close(ConnectionRef),
-	    ssh:stop_daemon(Pid)
-    end.
+%%--------------------------------------------------------------------
+%%% Test that post authentication compression option works
+exec_compressed_post_auth_compression(Config) when is_list(Config) ->
+    exec_compressed_helper(Config, 'zlib@openssh.com').
+
+%%--------------------------------------------------------------------
+%%% Exec compressed helper
+exec_compressed_helper(Config, CompressAlgorithm) ->
+    process_flag(trap_exit, true),
+    SystemDir = filename:join(proplists:get_value(priv_dir, Config), system),
+    UserDir = proplists:get_value(priv_dir, Config),
+
+    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir},{user_dir, UserDir},
+                                             {preferred_algorithms,[{compression, [CompressAlgorithm]}]},
+                                             {failfun, fun ssh_test_lib:failfun/2}]),
+
+    ConnectionRef =
+        ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
+                                          {user_dir, UserDir},
+                                          {user_interaction, false},
+                                          {preferred_algorithms,[{compression, [CompressAlgorithm]}]}]),
+    {ok, ChannelId} = ssh_connection:session_channel(ConnectionRef, infinity),
+    success = ssh_connection:exec(ConnectionRef, ChannelId,
+                                  "1+1.", infinity),
+    Data = {ssh_cm, ConnectionRef, {data, ChannelId, 0, <<"2">>}},
+    case ssh_test_lib:receive_exec_result(Data) of
+        expected ->
+            ok;
+        Other ->
+            ct:fail(Other)
+    end,
+    ssh_test_lib:receive_exec_end(ConnectionRef, ChannelId),
+    ssh:close(ConnectionRef),
+    ssh:stop_daemon(Pid).
 
 %%--------------------------------------------------------------------
 %%% Idle timeout test
Index: otp-OTP-27.1.3/lib/ssh/test/ssh_protocol_SUITE.erl
===================================================================
--- otp-OTP-27.1.3.orig/lib/ssh/test/ssh_protocol_SUITE.erl
+++ otp-OTP-27.1.3/lib/ssh/test/ssh_protocol_SUITE.erl
@@ -51,6 +51,10 @@
          client_handles_keyboard_interactive_0_pwds/1,
          client_handles_banner_keyboard_interactive/1,
          client_info_line/1,
+         decompression_bomb_client/1,
+         decompression_bomb_client_after_auth/1,
+         decompression_bomb_server/1,
+         decompression_bomb_server_after_auth/1,
          do_gex_client_init/3,
          do_gex_client_init_old/3,
          empty_service_name/1,
@@ -147,7 +151,11 @@ groups() ->
 		       lib_no_match
 		      ]},
      {packet_size_error, [], [packet_length_too_large,
-			      packet_length_too_short]},
+			      packet_length_too_short,
+			      decompression_bomb_client,
+			      decompression_bomb_client_after_auth,
+			      decompression_bomb_server,
+			      decompression_bomb_server_after_auth]},
      {field_size_error, [], [service_name_length_too_large,
 			     service_name_length_too_short]},
      {kex, [], [custom_kexinit,
@@ -241,6 +249,8 @@ init_per_testcase(TC, Config) when TC ==
 		     [{preferred_algorithms,[{cipher,?DEFAULT_CIPHERS}
                                             ]}
 		      | Opts]);
+init_per_testcase(decompression_bomb_client, Config) ->
+    start_std_daemon(Config, [{preferred_algorithms, [{compression, ['zlib']}]}]);
 init_per_testcase(_TestCase, Config) ->
     check_std_daemon_works(Config, ?LINE).
 
@@ -256,6 +266,8 @@ end_per_testcase(TC, Config) when TC ==
 				  TC == gex_client_old_request_exact ;
 				  TC == gex_client_old_request_noexact ->
     stop_std_daemon(Config);
+end_per_testcase(decompression_bomb_client, Config) ->
+    stop_std_daemon(Config);
 end_per_testcase(_TestCase, Config) ->
     check_std_daemon_works(Config, ?LINE).
 
@@ -693,6 +705,138 @@ bad_packet_length(Config, LengthExcess)
 	  ], InitialState).
 
 %%%--------------------------------------------------------------------
+decompression_bomb_client(Config) ->
+    {ok, InitialState} = connect_and_kex(Config, ssh_trpt_test_lib:exec([]),
+                                         [{kex, [?DEFAULT_KEX]},
+                                          {cipher, ?DEFAULT_CIPHERS},
+                                          {compression, ['zlib']}]),
+    %% ?SSH_MAX_PACKET_SIZE - 9 is enough to trigger disconnect because Payload of ssh packet becomes:
+    %% 1 byte message identifier
+    %% 4 bytes length of data field
+    %% ?SSH_MAX_PACKET_SIZE - 9 bytes of data
+    %% This is longer than max decompressed Payload length which is ?SSH_MAX_PACKET_SIZE - 5
+    %% See more in ssh_transport:safe_zlib_inflate_loop
+    Data = binary:copy(<<0>>, ?SSH_MAX_PACKET_SIZE - 9),
+    {ok, _} =
+        ssh_trpt_test_lib:exec([
+                                {send, #ssh_msg_ignore{data = Data}},
+                                {match, disconnect(), receive_msg}
+                               ], InitialState).
+
+%%%--------------------------------------------------------------------
+decompression_bomb_client_after_auth(Config) ->
+    {ok, InitialState} = connect_and_kex(Config, ssh_trpt_test_lib:exec([]),
+                                         [{kex, [?DEFAULT_KEX]},
+                                          {cipher, ?DEFAULT_CIPHERS},
+                                          {compression, ['zlib@openssh.com']}]),
+    {User, Pwd} = server_user_password(Config),
+    {ok, AfterAuthState} =
+        ssh_trpt_test_lib:exec(
+          [{send, #ssh_msg_service_request{name = "ssh-userauth"}},
+           {match, #ssh_msg_service_accept{name = "ssh-userauth"}, receive_msg},
+           {send, #ssh_msg_userauth_request{user = User,
+                                            service = "ssh-connection",
+                                            method = "password",
+                                            data = <<?BOOLEAN(?FALSE),
+                                                     ?STRING(unicode:characters_to_binary(Pwd))>>
+                                           }},
+           {match, #ssh_msg_userauth_success{_='_'}, receive_msg}
+          ], InitialState),
+    %% See explanation in decompression_bomb_client
+    Data = binary:copy(<<0>>, ?SSH_MAX_PACKET_SIZE - 9),
+    {ok, _} =
+        ssh_trpt_test_lib:exec([
+                                {send, #ssh_msg_ignore{data = Data}},
+                                {match, disconnect(), receive_msg}
+                               ], AfterAuthState).
+
+%%%--------------------------------------------------------------------
+decompression_bomb_server(Config) ->
+    {ok, InitialState} = ssh_trpt_test_lib:exec(listen),
+    HostPort = ssh_trpt_test_lib:server_host_port(InitialState),
+    %% See explanation in decompression_bomb_client
+    Data = binary:copy(<<0>>, ?SSH_MAX_PACKET_SIZE - 9),
+    ServerPid =
+        spawn_link(
+          fun() ->
+                  {ok, _} =
+                      ssh_trpt_test_lib:exec(
+                        [{set_options, [print_ops, print_messages]},
+                         {accept, [{system_dir, system_dir(Config)},
+                                   {user_dir, user_dir(Config)},
+                                   {preferred_algorithms,[{kex, [?DEFAULT_KEX]},
+                                                          {cipher, ?DEFAULT_CIPHERS},
+                                                          {compression, ['zlib']}]}]},
+                         receive_hello,
+                         {send, hello},
+                         {send, ssh_msg_kexinit},
+                         {match, #ssh_msg_kexinit{_='_'}, receive_msg},
+                         {match, #ssh_msg_kexdh_init{_='_'}, receive_msg},
+                         {send, ssh_msg_kexdh_reply},
+                         {send, #ssh_msg_newkeys{}},
+                         {match, #ssh_msg_newkeys{_='_'}, receive_msg},
+                         {send, #ssh_msg_ignore{data = Data}},
+                         {match, disconnect(), receive_msg}
+                        ], InitialState)
+          end),
+    Ref = monitor(process, ServerPid),
+    {error, "Protocol error"} =
+        std_connect(HostPort, Config,
+                    [{silently_accept_hosts, true},
+                     {user_dir, user_dir(Config)},
+                     {user_interaction, false},
+                     {preferred_algorithms, [{compression,['zlib']}]}]),
+    receive
+        {'DOWN', Ref, process, ServerPid, normal} -> ok
+    end.
+
+%%%--------------------------------------------------------------------
+decompression_bomb_server_after_auth(Config) ->
+    {ok, InitialState} = ssh_trpt_test_lib:exec(listen),
+    HostPort = ssh_trpt_test_lib:server_host_port(InitialState),
+    %% See explanation in decompression_bomb_client
+    Data = binary:copy(<<0>>, ?SSH_MAX_PACKET_SIZE - 9),
+    ServerPid =
+        spawn_link(
+          fun() ->
+                  {ok ,_} =
+                      ssh_trpt_test_lib:exec(
+                        [{set_options, [print_ops, print_messages]},
+                         {accept, [{system_dir, system_dir(Config)},
+                                   {user_dir, user_dir(Config)},
+                                   {preferred_algorithms,[{kex, [?DEFAULT_KEX]},
+                                                          {cipher, ?DEFAULT_CIPHERS},
+                                                          {compression, ['zlib@openssh.com']}]}]},
+                         receive_hello,
+                         {send, hello},
+                         {send, ssh_msg_kexinit},
+                         {match, #ssh_msg_kexinit{_='_'}, receive_msg},
+                         {match, #ssh_msg_kexdh_init{_='_'}, receive_msg},
+                         {send, ssh_msg_kexdh_reply},
+                         {send, #ssh_msg_newkeys{}},
+                         {match, #ssh_msg_newkeys{_='_'}, receive_msg},
+                         {match, #ssh_msg_service_request{name="ssh-userauth"}, receive_msg},
+                         {send, #ssh_msg_service_accept{name="ssh-userauth"}},
+                         {match, #ssh_msg_userauth_request{service="ssh-connection",
+                                                           method="none",
+                                                           _='_'}, receive_msg},
+                         {send, #ssh_msg_userauth_success{}},
+                         {send, #ssh_msg_ignore{data = Data}},
+                         {match, disconnect(), receive_msg}
+                        ], InitialState)
+          end),
+    Ref = monitor(process, ServerPid),
+    {ok, _} =
+        std_connect(HostPort, Config,
+                    [{silently_accept_hosts, true},
+                     {user_dir, user_dir(Config)},
+                     {user_interaction, false},
+                     {preferred_algorithms, [{compression, ['zlib@openssh.com']}]}]),
+    receive
+        {'DOWN', Ref, process, ServerPid, normal} -> ok
+    end.
+
+%%%--------------------------------------------------------------------
 service_name_length_too_large(Config) -> bad_service_name_length(Config, +4).
 
 service_name_length_too_short(Config) -> bad_service_name_length(Config, -4).
@@ -1565,17 +1709,19 @@ connect_and_kex(Config) ->
     connect_and_kex(Config, ssh_trpt_test_lib:exec([]) ).
 
 connect_and_kex(Config, InitialState) ->
+    ClientAlgs = [{kex,[?DEFAULT_KEX]}, {cipher,?DEFAULT_CIPHERS}],
+    connect_and_kex(Config, InitialState, ClientAlgs).
+
+connect_and_kex(Config, InitialState, ClientAlgs) ->
     ssh_trpt_test_lib:exec(
       [{connect,
-	server_host(Config),server_port(Config),
-	[{preferred_algorithms,[{kex,[?DEFAULT_KEX]},
-                                {cipher,?DEFAULT_CIPHERS}
-                               ]},
+        server_host(Config),server_port(Config),
+        [{preferred_algorithms,ClientAlgs},
          {silently_accept_hosts, true},
          {recv_ext_info, false},
-	 {user_dir, user_dir(Config)},
-	 {user_interaction, false}
-         | proplists:get_value(extra_options,Config,[])
+         {user_dir, user_dir(Config)},
+         {user_interaction, false}
+        | proplists:get_value(extra_options,Config,[])
         ]},
        receive_hello,
        {send, hello},
Index: otp-OTP-27.1.3/lib/ssh/test/ssh_trpt_test_lib.erl
===================================================================
--- otp-OTP-27.1.3.orig/lib/ssh/test/ssh_trpt_test_lib.erl
+++ otp-OTP-27.1.3/lib/ssh/test/ssh_trpt_test_lib.erl
@@ -447,7 +447,13 @@ send(S0, #ssh_msg_newkeys{} = Msg) ->
 	    fun(X) when X==true;X==detail -> {"Send~n~s~n",[format_msg(Msg)]} end),
     {ok, Packet, C} = ssh_transport:new_keys_message(S#s.ssh),
     send_bytes(Packet, S#s{ssh = C});
-    
+
+send(S0, #ssh_msg_userauth_success{} = Msg) ->
+    S = opt(print_messages, S0,
+        fun(X) when X==true;X==detail -> {"Send~n~s~n",[format_msg(Msg)]} end),
+    {Packet, C} = ssh_transport:ssh_packet(Msg, S#s.ssh),
+    send_bytes(Packet, S#s{ssh = C#ssh{authenticated = true}, return_value = Msg});
+
 send(S0, Msg) when is_tuple(Msg) ->
     S = opt(print_messages, S0,
 	    fun(X) when X==true;X==detail -> {"Send~n~s~n",[format_msg(Msg)]} end),
@@ -513,6 +519,9 @@ recv(S0 = #s{}) ->
 		#ssh_msg_newkeys{} ->
 		    {ok, C} = ssh_transport:handle_new_keys(PeerMsg, S#s.ssh),
 		    S#s{ssh=C};
+		#ssh_msg_userauth_success{} -> % Always the client
+		    C = S#s.ssh,
+		    S#s{ssh = C#ssh{authenticated = true}};
 		_ ->
 		    S
 	    end
Index: otp-OTP-27.1.3/lib/ssh/doc/guides/configurations.md
===================================================================
--- otp-OTP-27.1.3.orig/lib/ssh/doc/guides/configurations.md
+++ otp-OTP-27.1.3/lib/ssh/doc/guides/configurations.md
@@ -185,8 +185,8 @@ Eshell V10.6.4  (abort with ^G)
                        'hmac-sha1']},
        {server2client,['hmac-sha2-256','hmac-sha2-512',
                        'hmac-sha1']}]},
- {compression,[{client2server,[none,'zlib@openssh.com',zlib]},
-               {server2client,[none,'zlib@openssh.com',zlib]}]}]
+ {compression,[{client2server,[none,'zlib@openssh.com']},
+               {server2client,[none,'zlib@openssh.com']}]}]
 ```
 
 Note that the algorithms in the file `ex2.config` is not yet applied. They will
@@ -202,8 +202,8 @@ ok
           {server2client,['aes192-ctr']}]},
  {mac,[{client2server,['hmac-sha1']},
        {server2client,['hmac-sha1']}]},
- {compression,[{client2server,[none,'zlib@openssh.com',zlib]},
-               {server2client,[none,'zlib@openssh.com',zlib]}]}]
+ {compression,[{client2server,[none,'zlib@openssh.com']},
+               {server2client,[none,'zlib@openssh.com']}]}]
 4>
 ```
 
Index: otp-OTP-27.1.3/lib/ssh/doc/guides/configure_algos.md
===================================================================
--- otp-OTP-27.1.3.orig/lib/ssh/doc/guides/configure_algos.md
+++ otp-OTP-27.1.3/lib/ssh/doc/guides/configure_algos.md
@@ -78,7 +78,9 @@ The lists are (named as in the SSH appli
   This list is also divided into two for the both directions
 
 - **`compression`** - If and how to compress the message. Examples are `none`,
-  that is, no compression and `zlib`.
+  that is, no compression,
+  `zlib` for pre-authentication compression (disabled by default),
+  and `'zlib@openssh.com'` for post-authentication compression.
 
   This list is also divided into two for the both directions
 
@@ -120,8 +122,8 @@ There is an important command to list th
                        'hmac-sha1']},
        {server2client,['hmac-sha2-256','hmac-sha2-512',
                        'hmac-sha1']}]},
- {compression,[{client2server,[none,'zlib@openssh.com',zlib]},
-               {server2client,[none,'zlib@openssh.com',zlib]}]}]
+ {compression,[{client2server,[none,'zlib@openssh.com']},
+               {server2client,[none,'zlib@openssh.com']}]}]
 ```
 
 {: #example_default_algorithms }
@@ -174,8 +176,8 @@ Replace the kex algorithms list with the
                        'hmac-sha1']},
        {server2client,['hmac-sha2-256','hmac-sha2-512',
                        'hmac-sha1']}]},
- {compression,[{client2server,[none,'zlib@openssh.com',zlib]},
-               {server2client,[none,'zlib@openssh.com',zlib]}]}]
+ {compression,[{client2server,[none,'zlib@openssh.com']},
+               {server2client,[none,'zlib@openssh.com']}]}]
 ```
 
 Note that the unmentioned lists (`public_key`, `cipher`, `mac` and
@@ -209,8 +211,8 @@ possible to change both directions at on
                        'hmac-sha1']},
        {server2client,['hmac-sha2-256','hmac-sha2-512',
                        'hmac-sha1']}]},
- {compression,[{client2server,[none,'zlib@openssh.com',zlib]},
-               {server2client,[none,'zlib@openssh.com',zlib]}]}]
+ {compression,[{client2server,[none,'zlib@openssh.com']},
+               {server2client,[none,'zlib@openssh.com']}]}]
 ```
 
 Note that both lists in `cipher` has been changed to the provided value
@@ -246,8 +248,8 @@ possible to change only one of the direc
                        'hmac-sha1']},
        {server2client,['hmac-sha2-256','hmac-sha2-512',
                        'hmac-sha1']}]},
- {compression,[{client2server,[none,'zlib@openssh.com',zlib]},
-               {server2client,[none,'zlib@openssh.com',zlib]}]}]
+ {compression,[{client2server,[none,'zlib@openssh.com']},
+               {server2client,[none,'zlib@openssh.com']}]}]
 ```
 
 ### Example 4
@@ -341,8 +343,8 @@ supported according to [Supported algori
                        'hmac-sha1']},
        {server2client,['hmac-sha2-256','hmac-sha2-512',
                        'hmac-sha1']}]},
- {compression,[{client2server,[none,'zlib@openssh.com',zlib]},
-               {server2client,[none,'zlib@openssh.com',zlib]}]}]
+ {compression,[{client2server,[none,'zlib@openssh.com']},
+               {server2client,[none,'zlib@openssh.com']}]}]
 ```
 
 And the result shows that the Diffie-Hellman Group1 is added at the head of the
Index: otp-OTP-27.1.3/lib/ssh/doc/ssh_app.md
===================================================================
--- otp-OTP-27.1.3.orig/lib/ssh/doc/ssh_app.md
+++ otp-OTP-27.1.3/lib/ssh/doc/ssh_app.md
@@ -231,7 +231,14 @@ cipher aes256-gcm@openssh.com is negotia
 **Compression algorithms**
   - none
   - zlib@openssh.com
-  - zlib
+
+The following compression algorithm is disabled by default:
+
+- (zlib)
+
+It can be enabled with the
+[preferred_algorithms](`t:ssh:preferred_algorithms_common_option/0`) or
+[modify_algorithms](`t:ssh:modify_algorithms_common_option/0`) options.
 
 ## Unicode support
 
Index: otp-OTP-27.1.3/lib/ssh/doc/guides/hardening.md
===================================================================
--- otp-OTP-27.1.3.orig/lib/ssh/doc/guides/hardening.md
+++ otp-OTP-27.1.3/lib/ssh/doc/guides/hardening.md
@@ -93,6 +93,26 @@ A figure clarifies when a timeout is sta
 
 ![SSH server timeouts](assets/ssh_timeouts.jpg "SSH server timeouts")
 
+### Resilience to compression-based attacks
+
+SSH supports compression of the data stream.
+
+Reasonable finite [max_sessions](`m:ssh#hardening_daemon_options-max_sessions`)
+option is highly recommended if compression is used to prevent excessive resource
+usage by the compression library.
+See [Counters and parallelism](#counters-and-parallelism).
+
+The `'zlib@openssh.com'` algorithm is recommended because it only activates
+after successful authentication.
+
+The `'zlib'` algorithm is not recommended because it activates before
+authentication completes, allowing unauthenticated clients to expose potential
+vulnerabilities in compression libraries, and increases attack surface of
+compression-based side-channel and traffic-analysis attacks.
+
+In both algorithms decompression is protected by a size limit that prevents
+excessive memory consumption.
+
 ## Verifying the remote daemon (server) in an SSH client
 
 Every SSH server presents a public key - the _host key_ \- to the client while
