Bug#1105009: Acknowledgement (bookworm-pu: package erlang/1:25.2.3+dfsg-1+deb12u2)
Hi!
I would like to amend the erlang/1:25.2.3+dfsg-1+deb12u2 with additional
patch which fixes CVE-2025-4748 (insufficient sanitizing of filepaths
when extracting files from archives, see [1]). I'm attaching the patch itself
and a cumulative difference to erlang/1:25.2.3+dfsg-1+deb12u1 which
is currently in Debian stable.
Cheers!
--
Sergei Golovan
diff --git a/debian/changelog b/debian/changelog
index 21f9a2b7b..04f8dd176 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,8 +1,10 @@
erlang (1:25.2.3+dfsg-1+deb12u2) bookworm-proposed-updates; urgency=medium
* ssh: fix strict KEX hardening (CVE-2025-46712) (closes: #1104963).
+ * zip: sanitize pathnames when extracting files with absolute pathnames
+ (CVE-2025-4748) (closes: #1107939).
- -- Sergei Golovan <sgolovan@debian.org> Fri, 09 May 2025 09:29:41 +0300
+ -- Sergei Golovan <sgolovan@debian.org> Thu, 26 Jun 2025 13:14:36 +0300
erlang (1:25.2.3+dfsg-1+deb12u1) bookworm-security; urgency=high
diff --git a/debian/patches/series b/debian/patches/series
index 77019206c..6ab90acb4 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -14,3 +14,4 @@ ssh-use-chars_limit-for-bad-packets-error-messages.patch
ssh-custom_kexinit-test-added.patch
ssh-early-RCE-fix.patch
ssh-strict-KEX-exchange-hardening.patch
+zip-sanitize-paths.patch
diff --git a/debian/patches/zip-sanitize-paths.patch b/debian/patches/zip-sanitize-paths.patch
new file mode 100644
index 000000000..804693b92
--- /dev/null
+++ b/debian/patches/zip-sanitize-paths.patch
@@ -0,0 +1,133 @@
+From: Lukas Backstrom <lukas@erlang.org>
+Date: Tue, 27 May 2025 21:50:01 +0200
+Subject: [PATCH] stdlib: Properly sanatize filenames when (un)zipping
+ According to the Zip APPNOTE filenames "MUST NOT contain a drive or
+ device letter, or a leading slash.". So we strip those when zipping
+ and unzipping.
+Origin: https://github.com/erlang/otp/commit/ee67d46285394db95133709cef74b0c462d665aa
+Bug-Debian: https://bugs.debian.org/1107939
+Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2025-4748
+
+--- a/lib/stdlib/src/zip.erl
++++ b/lib/stdlib/src/zip.erl
+@@ -826,12 +826,12 @@
+ get_filename({Name, _, _}, Type) ->
+ get_filename(Name, Type);
+ get_filename(Name, regular) ->
+- Name;
++ sanitize_filename(Name);
+ get_filename(Name, directory) ->
+ %% Ensure trailing slash
+ case lists:reverse(Name) of
+- [$/ | _Rev] -> Name;
+- Rev -> lists:reverse([$/ | Rev])
++ [$/ | _Rev] -> sanitize_filename(Name);
++ Rev -> sanitize_filename(lists:reverse([$/ | Rev]))
+ end.
+
+ add_cwd(_CWD, {_Name, _} = F) -> F;
+@@ -1531,12 +1531,25 @@
+ get_file_name_extra(FileNameLen, ExtraLen, B, GPFlag) ->
+ try
+ <<BFileName:FileNameLen/binary, BExtra:ExtraLen/binary>> = B,
+- {binary_to_chars(BFileName, GPFlag), BExtra}
++ {sanitize_filename(binary_to_chars(BFileName, GPFlag)), BExtra}
+ catch
+ _:_ ->
+ throw(bad_file_header)
+ end.
+
++sanitize_filename(Filename) ->
++ case filename:pathtype(Filename) of
++ relative -> Filename;
++ _ ->
++ %% With absolute or volumerelative, we drop the prefix and rejoin
++ %% the path to create a relative path
++ Relative = filename:join(tl(filename:split(Filename))),
++ error_logger:format("Illegal absolute path: ~ts, converting to ~ts~n",
++ [Filename, Relative]),
++ relative = filename:pathtype(Relative),
++ Relative
++ end.
++
+ %% get compressed or stored data
+ get_z_data(?DEFLATED, In0, FileName, CompSize, Input, Output, OpO, Z) ->
+ ok = zlib:inflateInit(Z, -?MAX_WBITS),
+--- a/lib/stdlib/test/zip_SUITE.erl
++++ b/lib/stdlib/test/zip_SUITE.erl
+@@ -22,7 +22,7 @@
+ -export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1,
+ init_per_group/2,end_per_group/2, borderline/1, atomic/1,
+ bad_zip/1, unzip_from_binary/1, unzip_to_binary/1,
+- zip_to_binary/1,
++ zip_to_binary/1, sanitize_filenames/1,
+ unzip_options/1, zip_options/1, list_dir_options/1, aliases/1,
+ openzip_api/1, zip_api/1, open_leak/1, unzip_jar/1,
+ unzip_traversal_exploit/1,
+@@ -40,7 +40,8 @@
+ unzip_to_binary, zip_to_binary, unzip_options,
+ zip_options, list_dir_options, aliases, openzip_api,
+ zip_api, open_leak, unzip_jar, compress_control, foldl,
+- unzip_traversal_exploit,fd_leak,unicode,test_zip_dir].
++ unzip_traversal_exploit,fd_leak,unicode,test_zip_dir,
++ sanitize_filenames].
+
+ groups() ->
+ [].
+@@ -90,22 +91,27 @@
+ {ok, Archive} = zip:zip(Archive, [Name]),
+ ok = file:delete(Name),
+
++ RelName = filename:join(tl(filename:split(Name))),
++
+ %% Verify listing and extracting.
+ {ok, [#zip_comment{comment = []},
+- #zip_file{name = Name,
++ #zip_file{name = RelName,
+ info = Info,
+ offset = 0,
+ comp_size = _}]} = zip:list_dir(Archive),
+ Size = Info#file_info.size,
+- {ok, [Name]} = zip:extract(Archive, [verbose]),
++ TempRelName = filename:join(TempDir, RelName),
++ {ok, [TempRelName]} = zip:extract(Archive, [verbose, {cwd, TempDir}]),
+
+- %% Verify contents of extracted file.
+- {ok, Bin} = file:read_file(Name),
+- true = match_byte_list(X0, binary_to_list(Bin)),
++ %% Verify that absolute file was not created
++ {error, enoent} = file:read_file(Name),
+
++ %% Verify that relative contents of extracted file.
++ {ok, Bin} = file:read_file(TempRelName),
++ true = match_byte_list(X0, binary_to_list(Bin)),
+
+ %% Verify that Unix zip can read it. (if we have a unix zip that is!)
+- zipinfo_match(Archive, Name),
++ zipinfo_match(Archive, RelName),
+
+ ok.
+
+@@ -1052,3 +1058,21 @@
+ end
+ end)().
+
++sanitize_filenames(Config) ->
++ RootDir = proplists:get_value(priv_dir, Config),
++ TempDir = filename:join(RootDir, "borderline"),
++ ok = file:make_dir(TempDir),
++
++ %% Create a zip archive /tmp/absolute in it
++ %% This file was created using the command below on Erlang/OTP 28.0
++ %% 1> rr(file), {ok, {_, Bin}} = zip:zip("absolute.zip", [{"/tmp/absolute",<<>>,#file_info{ type=regular, mtime={{1970,1,1},{0,0,0}}, size=0 }}], [memory]), rp(base64:encode(Bin)).
++ AbsZip = base64:decode(<<"UEsDBBQAAAAAAAAAIewAAAAAAAAAAAAAAAANAAAAL3RtcC9hYnNvbHV0ZVBLAQIUAxQAAAAAAAAAIewAAAAAAAAAAAAAAAANAAAAAAAAAAAAAACkAQAAAAAvdG1wL2Fic29sdXRlUEsFBgAAAAABAAEAOwAAACsAAAAAAA==">>),
++ Archive = filename:join(TempDir, "absolute.zip"),
++ ok = file:write_file(Archive, AbsZip),
++
++ TmpAbs = filename:join([TempDir, "tmp", "absolute"]),
++ {ok, [TmpAbs]} = zip:unzip(Archive, [verbose, {cwd, TempDir}]),
++ {error, enoent} = file:read_file("/tmp/absolute"),
++ {ok, <<>>} = file:read_file(TmpAbs),
++
++ ok.
+\ No newline at end of file
diff --git a/debian/changelog b/debian/changelog
index d269d536f..04f8dd176 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,11 @@
+erlang (1:25.2.3+dfsg-1+deb12u2) bookworm-proposed-updates; urgency=medium
+
+ * ssh: fix strict KEX hardening (CVE-2025-46712) (closes: #1104963).
+ * zip: sanitize pathnames when extracting files with absolute pathnames
+ (CVE-2025-4748) (closes: #1107939).
+
+ -- Sergei Golovan <sgolovan@debian.org> Thu, 26 Jun 2025 13:14:36 +0300
+
erlang (1:25.2.3+dfsg-1+deb12u1) bookworm-security; urgency=high
[ Salvatore Bonaccorso ]
diff --git a/debian/patches/series b/debian/patches/series
index 68d1a9d42..6ab90acb4 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -13,3 +13,5 @@ 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
+ssh-strict-KEX-exchange-hardening.patch
+zip-sanitize-paths.patch
diff --git a/debian/patches/ssh-strict-KEX-exchange-hardening.patch b/debian/patches/ssh-strict-KEX-exchange-hardening.patch
new file mode 100644
index 000000000..e6b5f8444
--- /dev/null
+++ b/debian/patches/ssh-strict-KEX-exchange-hardening.patch
@@ -0,0 +1,607 @@
+From: Jakub Witczak <kuba@erlang.org>
+Date: Tue, 6 May 2025 17:01:29 +0200
+Subject: ssh: KEX strict implementation fixes
+ - fixed KEX strict implementation
+ - draft-miller-sshm-strict-kex-01.txt
+ - ssh_dbg added to ssh_fsm_kexinit module
+ - CVE-2025-46712
+Origin: https://github.com/erlang/otp/commit/e4b56a9f4a511aa9990dd86c16c61439c828df83
+Bug-Debian: https://bugs.debian.org/1104963
+Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2025-46712
+
+--- a/lib/ssh/src/ssh_connection_handler.erl
++++ b/lib/ssh/src/ssh_connection_handler.erl
+@@ -34,7 +34,6 @@
+ -include("ssh_transport.hrl").
+ -include("ssh_auth.hrl").
+ -include("ssh_connect.hrl").
+-
+ -include("ssh_fsm.hrl").
+
+ %%====================================================================
+@@ -705,16 +704,6 @@
+ 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;
+
+@@ -1118,11 +1107,14 @@
+ of
+ {packet_decrypted, DecryptedBytes, EncryptedDataRest, Ssh1} ->
+ D1 = D0#data{ssh_params =
+- Ssh1#ssh{recv_sequence = ssh_transport:next_seqnum(Ssh1#ssh.recv_sequence)},
+- decrypted_data_buffer = <<>>,
+- undecrypted_packet_length = undefined,
+- aead_data = <<>>,
+- encrypted_data_buffer = EncryptedDataRest},
++ Ssh1#ssh{recv_sequence =
++ ssh_transport:next_seqnum(StateName,
++ Ssh1#ssh.recv_sequence,
++ SshParams)},
++ decrypted_data_buffer = <<>>,
++ undecrypted_packet_length = undefined,
++ aead_data = <<>>,
++ encrypted_data_buffer = EncryptedDataRest},
+ try
+ ssh_message:decode(set_kex_overload_prefix(DecryptedBytes,D1))
+ of
+--- a/lib/ssh/src/ssh_fsm_kexinit.erl
++++ b/lib/ssh/src/ssh_fsm_kexinit.erl
+@@ -43,6 +43,11 @@
+ -export([callback_mode/0, handle_event/4, terminate/3,
+ format_status/2, code_change/4]).
+
++-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]).
++
+ %%====================================================================
+ %% gen_statem callbacks
+ %%====================================================================
+@@ -53,8 +58,13 @@
+
+ %%--------------------------------------------------------------------
+
+-%%% ######## {kexinit, client|server, init|renegotiate} ####
+
++handle_event(Type, Event = prepare_next_packet, StateName, D) ->
++ ssh_connection_handler:handle_event(Type, Event, StateName, D);
++handle_event(Type, Event = {send_disconnect, _, _, _, _}, StateName, D) ->
++ ssh_connection_handler:handle_event(Type, Event, StateName, D);
++
++%%% ######## {kexinit, client|server, init|renegotiate} ####
+ 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),
+@@ -67,11 +77,10 @@
+ end,
+ {next_state, {key_exchange,Role,ReNeg}, D#data{ssh_params=Ssh}};
+
+-
+ %%% ######## {key_exchange, client|server, init|renegotiate} ####
+-
+ %%%---- diffie-hellman
+ handle_event(internal, #ssh_msg_kexdh_init{} = Msg, {key_exchange,server,ReNeg}, D) ->
++ ok = check_kex_strict(Msg, D),
+ {ok, KexdhReply, Ssh1} = ssh_transport:handle_kexdh_init(Msg, D#data.ssh_params),
+ ssh_connection_handler:send_bytes(KexdhReply, D),
+ {ok, NewKeys, Ssh2} = ssh_transport:new_keys_message(Ssh1),
+@@ -81,6 +90,7 @@
+ {next_state, {new_keys,server,ReNeg}, D#data{ssh_params=Ssh}};
+
+ handle_event(internal, #ssh_msg_kexdh_reply{} = Msg, {key_exchange,client,ReNeg}, D) ->
++ ok = check_kex_strict(Msg, D),
+ {ok, NewKeys, Ssh1} = ssh_transport:handle_kexdh_reply(Msg, D#data.ssh_params),
+ ssh_connection_handler:send_bytes(NewKeys, D),
+ {ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh1),
+@@ -89,24 +99,28 @@
+
+ %%%---- diffie-hellman group exchange
+ handle_event(internal, #ssh_msg_kex_dh_gex_request{} = Msg, {key_exchange,server,ReNeg}, D) ->
++ ok = check_kex_strict(Msg, D),
+ {ok, GexGroup, Ssh1} = ssh_transport:handle_kex_dh_gex_request(Msg, D#data.ssh_params),
+ ssh_connection_handler:send_bytes(GexGroup, D),
+ Ssh = ssh_transport:parallell_gen_key(Ssh1),
+ {next_state, {key_exchange_dh_gex_init,server,ReNeg}, D#data{ssh_params=Ssh}};
+
+ handle_event(internal, #ssh_msg_kex_dh_gex_request_old{} = Msg, {key_exchange,server,ReNeg}, D) ->
++ ok = check_kex_strict(Msg, D),
+ {ok, GexGroup, Ssh1} = ssh_transport:handle_kex_dh_gex_request(Msg, D#data.ssh_params),
+ ssh_connection_handler:send_bytes(GexGroup, D),
+ Ssh = ssh_transport:parallell_gen_key(Ssh1),
+ {next_state, {key_exchange_dh_gex_init,server,ReNeg}, D#data{ssh_params=Ssh}};
+
+ handle_event(internal, #ssh_msg_kex_dh_gex_group{} = Msg, {key_exchange,client,ReNeg}, D) ->
++ ok = check_kex_strict(Msg, D),
+ {ok, KexGexInit, Ssh} = ssh_transport:handle_kex_dh_gex_group(Msg, D#data.ssh_params),
+ ssh_connection_handler:send_bytes(KexGexInit, D),
+ {next_state, {key_exchange_dh_gex_reply,client,ReNeg}, D#data{ssh_params=Ssh}};
+
+ %%%---- elliptic curve diffie-hellman
+ handle_event(internal, #ssh_msg_kex_ecdh_init{} = Msg, {key_exchange,server,ReNeg}, D) ->
++ ok = check_kex_strict(Msg, D),
+ {ok, KexEcdhReply, Ssh1} = ssh_transport:handle_kex_ecdh_init(Msg, D#data.ssh_params),
+ ssh_connection_handler:send_bytes(KexEcdhReply, D),
+ {ok, NewKeys, Ssh2} = ssh_transport:new_keys_message(Ssh1),
+@@ -116,16 +130,25 @@
+ {next_state, {new_keys,server,ReNeg}, D#data{ssh_params=Ssh}};
+
+ handle_event(internal, #ssh_msg_kex_ecdh_reply{} = Msg, {key_exchange,client,ReNeg}, D) ->
++ ok = check_kex_strict(Msg, D),
+ {ok, NewKeys, Ssh1} = ssh_transport:handle_kex_ecdh_reply(Msg, D#data.ssh_params),
+ ssh_connection_handler:send_bytes(NewKeys, D),
+ {ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh1),
+ ssh_connection_handler:send_bytes(ExtInfo, D),
+ {next_state, {new_keys,client,ReNeg}, D#data{ssh_params=Ssh}};
+
++%%% ######## handle KEX strict
++handle_event(internal, _Event, {key_exchange,_Role,init},
++ #data{ssh_params = #ssh{algorithms = #alg{kex_strict_negotiated = true},
++ send_sequence = SendSeq,
++ recv_sequence = RecvSeq}}) ->
++ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
++ io_lib:format("KEX strict violation: send_sequence = ~p recv_sequence = ~p",
++ [SendSeq, RecvSeq]));
+
+ %%% ######## {key_exchange_dh_gex_init, server, init|renegotiate} ####
+-
+ handle_event(internal, #ssh_msg_kex_dh_gex_init{} = Msg, {key_exchange_dh_gex_init,server,ReNeg}, D) ->
++ ok = check_kex_strict(Msg, D),
+ {ok, KexGexReply, Ssh1} = ssh_transport:handle_kex_dh_gex_init(Msg, D#data.ssh_params),
+ ssh_connection_handler:send_bytes(KexGexReply, D),
+ {ok, NewKeys, Ssh2} = ssh_transport:new_keys_message(Ssh1),
+@@ -133,20 +156,33 @@
+ {ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh2),
+ ssh_connection_handler:send_bytes(ExtInfo, D),
+ {next_state, {new_keys,server,ReNeg}, D#data{ssh_params=Ssh}};
+-
++%%% ######## handle KEX strict
++handle_event(internal, _Event, {key_exchange_dh_gex_init,_Role,init},
++ #data{ssh_params = #ssh{algorithms = #alg{kex_strict_negotiated = true},
++ send_sequence = SendSeq,
++ recv_sequence = RecvSeq}}) ->
++ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
++ io_lib:format("KEX strict violation: send_sequence = ~p recv_sequence = ~p",
++ [SendSeq, RecvSeq]));
+
+ %%% ######## {key_exchange_dh_gex_reply, client, init|renegotiate} ####
+-
+ handle_event(internal, #ssh_msg_kex_dh_gex_reply{} = Msg, {key_exchange_dh_gex_reply,client,ReNeg}, D) ->
++ ok = check_kex_strict(Msg, D),
+ {ok, NewKeys, Ssh1} = ssh_transport:handle_kex_dh_gex_reply(Msg, D#data.ssh_params),
+ ssh_connection_handler:send_bytes(NewKeys, D),
+ {ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh1),
+ ssh_connection_handler:send_bytes(ExtInfo, D),
+ {next_state, {new_keys,client,ReNeg}, D#data{ssh_params=Ssh}};
+-
++%%% ######## handle KEX strict
++handle_event(internal, _Event, {key_exchange_dh_gex_reply,_Role,init},
++ #data{ssh_params = #ssh{algorithms = #alg{kex_strict_negotiated = true},
++ send_sequence = SendSeq,
++ recv_sequence = RecvSeq}}) ->
++ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
++ io_lib:format("KEX strict violation: send_sequence = ~p recv_sequence = ~p",
++ [SendSeq, RecvSeq]));
+
+ %%% ######## {new_keys, client|server} ####
+-
+ %% First key exchange round:
+ handle_event(internal, #ssh_msg_newkeys{} = Msg, {new_keys,client,init}, D0) ->
+ {ok, Ssh1} = ssh_transport:handle_new_keys(Msg, D0#data.ssh_params),
+@@ -162,6 +198,15 @@
+ %% ssh_connection_handler:send_bytes(ExtInfo, D),
+ {next_state, {ext_info,server,init}, D#data{ssh_params=Ssh}};
+
++%%% ######## handle KEX strict
++handle_event(internal, _Event, {new_keys,_Role,init},
++ #data{ssh_params = #ssh{algorithms = #alg{kex_strict_negotiated = true},
++ send_sequence = SendSeq,
++ recv_sequence = RecvSeq}}) ->
++ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
++ io_lib:format("KEX strict violation (send_sequence = ~p recv_sequence = ~p)",
++ [SendSeq, RecvSeq]));
++
+ %% Subsequent key exchange rounds (renegotiation):
+ handle_event(internal, #ssh_msg_newkeys{} = Msg, {new_keys,Role,renegotiate}, D) ->
+ {ok, Ssh} = ssh_transport:handle_new_keys(Msg, D#data.ssh_params),
+@@ -183,7 +228,6 @@
+ handle_event(internal, #ssh_msg_newkeys{}=Msg, {ext_info,_Role,renegotiate}, D) ->
+ {ok, Ssh} = ssh_transport:handle_new_keys(Msg, D#data.ssh_params),
+ {keep_state, D#data{ssh_params = Ssh}};
+-
+
+ handle_event(internal, Msg, {ext_info,Role,init}, D) when is_tuple(Msg) ->
+ %% If something else arrives, goto next state and handle the event in that one
+@@ -217,3 +261,70 @@
+ peer_role(client) -> server;
+ peer_role(server) -> client.
+
++check_kex_strict(Msg,
++ #data{ssh_params =
++ #ssh{algorithms =
++ #alg{
++ kex = Kex,
++ kex_strict_negotiated = KexStrictNegotiated},
++ send_sequence = SendSeq,
++ recv_sequence = RecvSeq}}) ->
++ case check_msg_group(Msg, get_alg_group(Kex), KexStrictNegotiated) of
++ ok ->
++ ok;
++ error ->
++ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
++ io_lib:format("KEX strict violation: send_sequence = ~p recv_sequence = ~p",
++ [SendSeq, RecvSeq]))
++ end.
++
++get_alg_group(Kex) when Kex == 'diffie-hellman-group16-sha512';
++ Kex == 'diffie-hellman-group18-sha512';
++ Kex == 'diffie-hellman-group14-sha256';
++ Kex == 'diffie-hellman-group14-sha1';
++ Kex == 'diffie-hellman-group1-sha1' ->
++ dh_alg;
++get_alg_group(Kex) when Kex == 'diffie-hellman-group-exchange-sha256';
++ Kex == 'diffie-hellman-group-exchange-sha1' ->
++ dh_gex_alg;
++get_alg_group(Kex) when Kex == 'curve25519-sha256';
++ Kex == 'curve25519-sha256@libssh.org';
++ Kex == 'curve448-sha512';
++ Kex == 'ecdh-sha2-nistp521';
++ Kex == 'ecdh-sha2-nistp384';
++ Kex == 'ecdh-sha2-nistp256' ->
++ ecdh_alg.
++
++check_msg_group(_Msg, _AlgGroup, false) -> ok;
++check_msg_group(#ssh_msg_kexdh_init{}, dh_alg, true) -> ok;
++check_msg_group(#ssh_msg_kexdh_reply{}, dh_alg, true) -> ok;
++check_msg_group(#ssh_msg_kex_dh_gex_request_old{}, dh_gex_alg, true) -> ok;
++check_msg_group(#ssh_msg_kex_dh_gex_request{}, dh_gex_alg, true) -> ok;
++check_msg_group(#ssh_msg_kex_dh_gex_group{}, dh_gex_alg, true) -> ok;
++check_msg_group(#ssh_msg_kex_dh_gex_init{}, dh_gex_alg, true) -> ok;
++check_msg_group(#ssh_msg_kex_dh_gex_reply{}, dh_gex_alg, true) -> ok;
++check_msg_group(#ssh_msg_kex_ecdh_init{}, ecdh_alg, true) -> ok;
++check_msg_group(#ssh_msg_kex_ecdh_reply{}, ecdh_alg, true) -> ok;
++check_msg_group(_Msg, _AlgGroup, _) -> error.
++
++%%%################################################################
++%%%#
++%%%# Tracing
++%%%#
++
++ssh_dbg_trace_points() -> [connection_events].
++
++ssh_dbg_flags(connection_events) -> [c].
++
++ssh_dbg_on(connection_events) -> dbg:tp(?MODULE, handle_event, 4, x).
++
++ssh_dbg_off(connection_events) -> dbg:ctpg(?MODULE, handle_event, 4).
++
++ssh_dbg_format(connection_events, {call, {?MODULE,handle_event, [EventType, EventContent, State, _Data]}}) ->
++ ["Connection event\n",
++ io_lib:format("[~w] EventType: ~p~nEventContent: ~p~nState: ~p~n", [?MODULE, EventType, EventContent, State])
++ ];
++ssh_dbg_format(connection_events, {return_from, {?MODULE,handle_event,4}, Ret}) ->
++ ["Connection event result\n",
++ io_lib:format("[~w] ~p~n", [?MODULE, ssh_dbg:reduce_state(Ret, #data{})])
++ ].
+--- a/lib/ssh/src/ssh_transport.erl
++++ b/lib/ssh/src/ssh_transport.erl
+@@ -26,12 +26,11 @@
+
+ -include_lib("public_key/include/public_key.hrl").
+ -include_lib("kernel/include/inet.hrl").
+-
+ -include("ssh_transport.hrl").
+ -include("ssh.hrl").
+
+ -export([versions/2, hello_version_msg/1]).
+--export([next_seqnum/1,
++-export([next_seqnum/3,
+ supported_algorithms/0, supported_algorithms/1,
+ default_algorithms/0, default_algorithms/1,
+ clear_default_algorithms_env/0,
+@@ -297,7 +296,12 @@
+ hello_version_msg(Data) ->
+ [Data,"\r\n"].
+
+-next_seqnum(SeqNum) ->
++next_seqnum({State, _Role, init}, 16#ffffffff,
++ #ssh{algorithms = #alg{kex_strict_negotiated = true}})
++ when State == kexinit; State == key_exchange; State == new_keys ->
++ ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
++ io_lib:format("KEX strict violation: recv_sequence = 16#ffffffff", []));
++next_seqnum(_State, SeqNum, _) ->
+ (SeqNum + 1) band 16#ffffffff.
+
+ is_valid_mac(_, _ , #ssh{recv_mac_size = 0}) ->
+@@ -1082,7 +1086,7 @@
+ %% algorithm. Each string MUST contain at least one algorithm name.
+ select_algorithm(Role, Client, Server,
+ #ssh{opts = Opts,
+- kex_strict_negotiated = KexStrictNegotiated0},
++ kex_strict_negotiated = KexStrictNegotiated0},
+ ReNeg) ->
+ KexStrictNegotiated =
+ case ReNeg of
+@@ -1108,7 +1112,6 @@
+ _ ->
+ KexStrictNegotiated0
+ end,
+-
+ {Encrypt0, Decrypt0} = select_encrypt_decrypt(Role, Client, Server),
+ {SendMac0, RecvMac0} = select_send_recv_mac(Role, Client, Server),
+
+--- a/lib/ssh/test/ssh_protocol_SUITE.erl
++++ b/lib/ssh/test/ssh_protocol_SUITE.erl
+@@ -54,7 +54,9 @@
+ ext_info_c/1,
+ ext_info_s/1,
+ kex_strict_negotiated/1,
+- kex_strict_msg_ignore/1,
++ kex_strict_violation_key_exchange/1,
++ kex_strict_violation_new_keys/1,
++ kex_strict_violation/1,
+ kex_strict_msg_unknown/1,
+ gex_client_init_option_groups/1,
+ gex_client_init_option_groups_file/1,
+@@ -143,7 +145,9 @@
+ gex_client_old_request_exact,
+ gex_client_old_request_noexact,
+ kex_strict_negotiated,
+- kex_strict_msg_ignore,
++ kex_strict_violation_key_exchange,
++ kex_strict_violation_new_keys,
++ kex_strict_violation,
+ kex_strict_msg_unknown]},
+ {service_requests, [], [bad_service_name,
+ bad_long_service_name,
+@@ -930,22 +934,145 @@
+ 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 SSH message
++%% ssh_fsm_kexinit in key_exchange state
++kex_strict_violation_key_exchange(Config) ->
++ ExpectedReason = "KEX strict violation",
++ Injections = [ssh_msg_ignore, ssh_msg_debug, ssh_msg_unimplemented],
++ TestProcedure =
++ fun(M) ->
++ ct:log(
++ "=================== START: ~p Message: ~p Expected Fail =================================",
++ [?FUNCTION_NAME, M]),
++ [receive_hello,
++ {send, hello},
++ {send, ssh_msg_kexinit},
++ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
++ {send, M},
++ {match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}]
++ end,
++ [kex_strict_helper(Config, TestProcedure(Msg), ExpectedReason) ||
++ Msg <- Injections],
++ ct:log("========== END ========"),
++ ok.
++
++%% Connect to an erlang server and inject unexpected SSH message
++%% ssh_fsm_kexinit in new_keys state
++kex_strict_violation_new_keys(Config) ->
++ ExpectedReason = "KEX strict violation",
++ Injections = [ssh_msg_ignore, ssh_msg_debug, ssh_msg_unimplemented],
++ TestProcedure =
++ fun(M) ->
++ ct:log(
++ "=================== START: ~p Message: ~p Expected Fail =================================",
++ [?FUNCTION_NAME, M]),
++ [receive_hello,
++ {send, hello},
++ {send, ssh_msg_kexinit},
++ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
++ {send, ssh_msg_kexdh_init},
++ {send, M},
++ {match, #ssh_msg_kexdh_reply{_='_'}, receive_msg},
++ {match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}]
++ end,
++ [kex_strict_helper(Config, TestProcedure(Msg), ExpectedReason) ||
++ Msg <- Injections],
++ ct:log("========== END ========"),
++ ok.
++
++%% Connect to an erlang server and inject unexpected SSH message
++%% duplicated KEXINIT
++kex_strict_violation(Config) ->
++ KexDhReply =
++ #ssh_msg_kexdh_reply{
++ public_host_key = {{{'ECPoint',<<73,72,235,162,96,101,154,59,217,114,123,192,96,105,250,29,214,76,60,63,167,21,221,118,246,168,152,2,7,172,137,125>>},
++ {namedCurve,{1,3,101,112}}},
++ 'ssh-ed25519'},
++ f = 18504393053016436370762156176197081926381112956345797067569792020930728564439992620494295053804030674742529174859108487694089045521619258420515443400605141150065440678508889060925968846155921972385560196703381004650914261218463420313738628465563288022895912907728767735629532940627575655703806353550720122093175255090704443612257683903495753071530605378193139909567971489952258218767352348904221407081210633467414579377014704081235998044497191940270966762124544755076128392259615566530695493013708460088312025006678879288856957348606386230195080105197251789635675011844976120745546472873505352732719507783227210178188,
++ h_sig = <<90,247,44,240,136,196,82,215,56,165,53,33,230,101,253,
++ 34,112,201,21,131,162,169,10,129,174,14,69,25,39,174,
++ 92,210,130,249,103,2,215,245,7,213,110,235,136,134,11,
++ 124,248,139,79,17,225,77,125,182,204,84,137,167,99,186,
++ 167,42,192,10>>},
++ TestFlows =
++ [
++ {kexinit, "KEX strict violation",
++ [receive_hello,
++ {send, hello},
++ {send, ssh_msg_kexinit},
++ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
++ {send, ssh_msg_kexinit},
++ {match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}]},
++ {ssh_msg_kexdh_init, "KEX strict violation",
++ [receive_hello,
++ {send, hello},
++ {send, ssh_msg_kexinit},
++ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
++ {send, ssh_msg_kexdh_init_dup},
++ {match,# ssh_msg_kexdh_reply{_='_'}, receive_msg},
++ {match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}]},
++ {new_keys, "Message ssh_msg_newkeys in wrong state",
++ [receive_hello,
++ {send, hello},
++ {send, ssh_msg_kexinit},
++ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
++ {send, ssh_msg_kexdh_init},
++ {match,# ssh_msg_kexdh_reply{_='_'}, receive_msg},
++ {send, #ssh_msg_newkeys{}},
++ {match, #ssh_msg_newkeys{_='_'}, receive_msg},
++ {send, #ssh_msg_newkeys{}},
++ {match, disconnect(?SSH_DISCONNECT_PROTOCOL_ERROR), receive_msg}]},
++ {ssh_msg_unexpected_dh_gex, "KEX strict violation",
++ [receive_hello,
++ {send, hello},
++ {send, ssh_msg_kexinit},
++ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
++ %% dh_alg is expected but dh_gex_alg is provided
++ {send, #ssh_msg_kex_dh_gex_request{min = 1000, n = 3000, max = 4000}},
++ {match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}]},
++ {wrong_role, "KEX strict violation",
++ [receive_hello,
++ {send, hello},
++ {send, ssh_msg_kexinit},
++ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
++ %% client should not send message below
++ {send, KexDhReply},
++ {match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}]},
++ {wrong_role2, "KEX strict violation",
++ [receive_hello,
++ {send, hello},
++ {send, ssh_msg_kexinit},
++ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
++ {send, ssh_msg_kexdh_init},
++ {match,# ssh_msg_kexdh_reply{_='_'}, receive_msg},
++ %% client should not send message below
++ {send, KexDhReply},
++ {match, #ssh_msg_newkeys{_='_'}, receive_msg},
++ {match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}]}
++ ],
++ TestProcedure =
++ fun({Msg, _, P}) ->
++ ct:log(
++ "==== START: ~p (duplicated ~p) Expected Fail ====~n~p",
++ [?FUNCTION_NAME, Msg, P]),
++ P
++ end,
++ [kex_strict_helper(Config, TestProcedure(Procedure), Reason) ||
++ Procedure = {_, Reason, _} <- TestFlows],
++ ct:log("==== END ====="),
++ ok.
+
+ %% 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},
++ [receive_hello,
++ {send, hello},
++ {send, ssh_msg_kexinit},
++ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
++ {send, ssh_msg_kexdh_init},
++ {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).
+@@ -970,12 +1097,7 @@
+ {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),
+--- a/lib/ssh/test/ssh_trpt_test_lib.erl
++++ b/lib/ssh/test/ssh_trpt_test_lib.erl
+@@ -90,7 +90,8 @@
+ report_trace(throw, Term, S1),
+ throw({Term,Op});
+
+- error:Error ->
++ error:Error:St ->
++ ct:log("Stacktrace=~n~p", [St]),
+ report_trace(error, Error, S1),
+ error({Error,Op});
+
+@@ -335,6 +336,17 @@
+ Msg = #ssh_msg_ignore{data = "unexpected_ignore_message"},
+ send(S0, Msg);
+
++send(S0, ssh_msg_debug) ->
++ Msg = #ssh_msg_debug{
++ always_display = true,
++ message = "some debug message",
++ language = "en"},
++ send(S0, Msg);
++
++send(S0, ssh_msg_unimplemented) ->
++ Msg = #ssh_msg_unimplemented{sequence = 123},
++ send(S0, Msg);
++
+ send(S0, ssh_msg_unknown) ->
+ Msg = binary:encode_hex(<<"0000000C060900000000000000000000">>),
+ send(S0, Msg);
+@@ -382,6 +394,26 @@
+ end),
+ send_bytes(NextKexMsgBin, S#s{ssh = C});
+
++send(S0, ssh_msg_kexdh_init_dup) when ?role(S0) == client ->
++ {OwnMsg, PeerMsg} = S0#s.alg_neg,
++ {ok, NextKexMsgBin, C} =
++ try ssh_transport:handle_kexinit_msg(PeerMsg, OwnMsg, S0#s.ssh, init)
++ catch
++ Class:Exc ->
++ fail("Algorithm negotiation failed!",
++ {"Algorithm negotiation failed at line ~p:~p~n~p:~s~nPeer: ~s~n Own: ~s",
++ [?MODULE,?LINE,Class,format_msg(Exc),format_msg(PeerMsg),format_msg(OwnMsg)]},
++ S0)
++ end,
++ S = opt(print_messages, S0,
++ fun(X) when X==true;X==detail ->
++ #ssh{keyex_key = {{_Private, Public}, {_G, _P}}} = C,
++ Msg = #ssh_msg_kexdh_init{e = Public},
++ {"Send (reconstructed)~n~s~n",[format_msg(Msg)]}
++ end),
++ send_bytes(NextKexMsgBin, S#s{ssh = C}),
++ send_bytes(NextKexMsgBin, S#s{ssh = C});
++
+ send(S0, ssh_msg_kexdh_reply) ->
+ Bytes = proplists:get_value(ssh_msg_kexdh_reply, S0#s.reply),
+ S = opt(print_messages, S0,
+@@ -531,7 +563,10 @@
+ S0#s.ssh)
+ of
+ {packet_decrypted, DecryptedBytes, EncryptedDataRest, Ssh1} ->
+- S1 = S0#s{ssh = Ssh1#ssh{recv_sequence = ssh_transport:next_seqnum(Ssh1#ssh.recv_sequence)},
++ S1 = S0#s{ssh = Ssh1#ssh{recv_sequence =
++ ssh_transport:next_seqnum(undefined,
++ Ssh1#ssh.recv_sequence,
++ false)},
+ decrypted_data_buffer = <<>>,
+ undecrypted_packet_length = undefined,
+ aead_data = <<>>,
diff --git a/debian/patches/zip-sanitize-paths.patch b/debian/patches/zip-sanitize-paths.patch
new file mode 100644
index 000000000..804693b92
--- /dev/null
+++ b/debian/patches/zip-sanitize-paths.patch
@@ -0,0 +1,133 @@
+From: Lukas Backstrom <lukas@erlang.org>
+Date: Tue, 27 May 2025 21:50:01 +0200
+Subject: [PATCH] stdlib: Properly sanatize filenames when (un)zipping
+ According to the Zip APPNOTE filenames "MUST NOT contain a drive or
+ device letter, or a leading slash.". So we strip those when zipping
+ and unzipping.
+Origin: https://github.com/erlang/otp/commit/ee67d46285394db95133709cef74b0c462d665aa
+Bug-Debian: https://bugs.debian.org/1107939
+Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2025-4748
+
+--- a/lib/stdlib/src/zip.erl
++++ b/lib/stdlib/src/zip.erl
+@@ -826,12 +826,12 @@
+ get_filename({Name, _, _}, Type) ->
+ get_filename(Name, Type);
+ get_filename(Name, regular) ->
+- Name;
++ sanitize_filename(Name);
+ get_filename(Name, directory) ->
+ %% Ensure trailing slash
+ case lists:reverse(Name) of
+- [$/ | _Rev] -> Name;
+- Rev -> lists:reverse([$/ | Rev])
++ [$/ | _Rev] -> sanitize_filename(Name);
++ Rev -> sanitize_filename(lists:reverse([$/ | Rev]))
+ end.
+
+ add_cwd(_CWD, {_Name, _} = F) -> F;
+@@ -1531,12 +1531,25 @@
+ get_file_name_extra(FileNameLen, ExtraLen, B, GPFlag) ->
+ try
+ <<BFileName:FileNameLen/binary, BExtra:ExtraLen/binary>> = B,
+- {binary_to_chars(BFileName, GPFlag), BExtra}
++ {sanitize_filename(binary_to_chars(BFileName, GPFlag)), BExtra}
+ catch
+ _:_ ->
+ throw(bad_file_header)
+ end.
+
++sanitize_filename(Filename) ->
++ case filename:pathtype(Filename) of
++ relative -> Filename;
++ _ ->
++ %% With absolute or volumerelative, we drop the prefix and rejoin
++ %% the path to create a relative path
++ Relative = filename:join(tl(filename:split(Filename))),
++ error_logger:format("Illegal absolute path: ~ts, converting to ~ts~n",
++ [Filename, Relative]),
++ relative = filename:pathtype(Relative),
++ Relative
++ end.
++
+ %% get compressed or stored data
+ get_z_data(?DEFLATED, In0, FileName, CompSize, Input, Output, OpO, Z) ->
+ ok = zlib:inflateInit(Z, -?MAX_WBITS),
+--- a/lib/stdlib/test/zip_SUITE.erl
++++ b/lib/stdlib/test/zip_SUITE.erl
+@@ -22,7 +22,7 @@
+ -export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1,
+ init_per_group/2,end_per_group/2, borderline/1, atomic/1,
+ bad_zip/1, unzip_from_binary/1, unzip_to_binary/1,
+- zip_to_binary/1,
++ zip_to_binary/1, sanitize_filenames/1,
+ unzip_options/1, zip_options/1, list_dir_options/1, aliases/1,
+ openzip_api/1, zip_api/1, open_leak/1, unzip_jar/1,
+ unzip_traversal_exploit/1,
+@@ -40,7 +40,8 @@
+ unzip_to_binary, zip_to_binary, unzip_options,
+ zip_options, list_dir_options, aliases, openzip_api,
+ zip_api, open_leak, unzip_jar, compress_control, foldl,
+- unzip_traversal_exploit,fd_leak,unicode,test_zip_dir].
++ unzip_traversal_exploit,fd_leak,unicode,test_zip_dir,
++ sanitize_filenames].
+
+ groups() ->
+ [].
+@@ -90,22 +91,27 @@
+ {ok, Archive} = zip:zip(Archive, [Name]),
+ ok = file:delete(Name),
+
++ RelName = filename:join(tl(filename:split(Name))),
++
+ %% Verify listing and extracting.
+ {ok, [#zip_comment{comment = []},
+- #zip_file{name = Name,
++ #zip_file{name = RelName,
+ info = Info,
+ offset = 0,
+ comp_size = _}]} = zip:list_dir(Archive),
+ Size = Info#file_info.size,
+- {ok, [Name]} = zip:extract(Archive, [verbose]),
++ TempRelName = filename:join(TempDir, RelName),
++ {ok, [TempRelName]} = zip:extract(Archive, [verbose, {cwd, TempDir}]),
+
+- %% Verify contents of extracted file.
+- {ok, Bin} = file:read_file(Name),
+- true = match_byte_list(X0, binary_to_list(Bin)),
++ %% Verify that absolute file was not created
++ {error, enoent} = file:read_file(Name),
+
++ %% Verify that relative contents of extracted file.
++ {ok, Bin} = file:read_file(TempRelName),
++ true = match_byte_list(X0, binary_to_list(Bin)),
+
+ %% Verify that Unix zip can read it. (if we have a unix zip that is!)
+- zipinfo_match(Archive, Name),
++ zipinfo_match(Archive, RelName),
+
+ ok.
+
+@@ -1052,3 +1058,21 @@
+ end
+ end)().
+
++sanitize_filenames(Config) ->
++ RootDir = proplists:get_value(priv_dir, Config),
++ TempDir = filename:join(RootDir, "borderline"),
++ ok = file:make_dir(TempDir),
++
++ %% Create a zip archive /tmp/absolute in it
++ %% This file was created using the command below on Erlang/OTP 28.0
++ %% 1> rr(file), {ok, {_, Bin}} = zip:zip("absolute.zip", [{"/tmp/absolute",<<>>,#file_info{ type=regular, mtime={{1970,1,1},{0,0,0}}, size=0 }}], [memory]), rp(base64:encode(Bin)).
++ AbsZip = base64:decode(<<"UEsDBBQAAAAAAAAAIewAAAAAAAAAAAAAAAANAAAAL3RtcC9hYnNvbHV0ZVBLAQIUAxQAAAAAAAAAIewAAAAAAAAAAAAAAAANAAAAAAAAAAAAAACkAQAAAAAvdG1wL2Fic29sdXRlUEsFBgAAAAABAAEAOwAAACsAAAAAAA==">>),
++ Archive = filename:join(TempDir, "absolute.zip"),
++ ok = file:write_file(Archive, AbsZip),
++
++ TmpAbs = filename:join([TempDir, "tmp", "absolute"]),
++ {ok, [TmpAbs]} = zip:unzip(Archive, [verbose, {cwd, TempDir}]),
++ {error, enoent} = file:read_file("/tmp/absolute"),
++ {ok, <<>>} = file:read_file(TmpAbs),
++
++ ok.
+\ No newline at end of file
Reply to: