Fix information disclosure via MITM attack bypassing TLS in net-imap.

Fixes CVE-2026-42246.

Backport https://github.com/ruby/net-imap/pull/667 for net-imap 0.3.10,
it is closest to Ruby 2.5.9's net-imap.

Resolves: RHEL-181772
This commit is contained in:
Jarek Prokop 2026-06-11 18:22:07 +02:00
parent 5ba469107d
commit 9a7a073556
2 changed files with 344 additions and 1 deletions

View File

@ -21,7 +21,7 @@
%endif
%global release 114
%global release 115
%{!?release_string:%global release_string %{?development_release:0.}%{release}%{?development_release:.%{development_release}}%{?dist}}
@ -275,6 +275,11 @@ Patch50: rubygem-rexml-3.3.9-Fix-ReDoS-CVE-2024-49761.patch
# https://github.com/kkos/oniguruma/issues/164#issuecomment-558134827
# https://issues.redhat.com/browse/RHEL-87505
Patch51: ruby-3.5.0-fix-164-Integer-overflow-related-to-reg-dmax-in-sear.patch
# Fix CVE-2026-42246 Information disclosure via man-in-the-middle attack bypassing TLS.
# Backport https://github.com/ruby/net-imap/pull/667 for net-imap 0.3.10,
# it is closest to Ruby 2.5.9's net-imap.
# For details, see commit notes 3 equal signs `===` in the patch.
Patch52: rubygem-net-imap-0.3.10-Fix-Information-disclosure-via-man-in-the-middle-attack-bypassing-TLS-CVE-2026-42246.patch
Requires: %{name}-libs%{?_isa} = %{version}-%{release}
@ -697,6 +702,7 @@ sed -i 's/"evaluation\/incorrect_words.yaml"\.freeze, //' \
%patch49 -p1
%patch50 -p1
%patch51 -p1
%patch52 -p1
# Provide an example of usage of the tapset:
cp -a %{SOURCE3} .
@ -1261,6 +1267,11 @@ OPENSSL_SYSTEM_CIPHERS_OVERRIDE=xyz_nonexistent_file OPENSSL_CONF='' \
%{gem_dir}/specifications/xmlrpc-%{xmlrpc_version}.gemspec
%changelog
* Thu Jun 11 2026 Jarek Prokop <jprokop@redhat.com> - 2.5.9-115
- Fix information disclosure via MITM attack bypassing TLS in net-imap.
(CVE-2026-42246)
Resolves: RHEL-181772
* Mon May 05 2025 Vít Ondruch <vondruch@redhat.com> - 2.5.9-114
- Fix integer overflow in search_in_range function in regexec.c (CVE-2019-19012).
Resolves: RHEL-87505

View File

@ -0,0 +1,332 @@
From ac248b06de768b68120b910b2977d55320871b0e Mon Sep 17 00:00:00 2001
From: nick evans <nick@rubinick.dev>
Date: Sun, 29 Mar 2026 12:26:48 -0400
Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=8D=92=20pick=20257ede0df:=20?=
=?UTF-8?q?=F0=9F=A5=85=20Re-raise=20`#starttls`=20error=20from=20receiver?=
=?UTF-8?q?=20thread=20[backports=20#395]?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Backports #395 to `v0.3-stable`. The tests required an additional
rescue-and-ignore for the server thread in `starttls_test`, which was
already present in all later branches.
---------
When `start_tls_session` raises an exception, that's caught in the
receiver thread, but not re-raised. Fortunately, `@sock` will now be
a permanently broken SSLSocket, so I don't think this can lead to
accidentally using an insecure connection.
Even so, `#starttls` should disconnect the socket and re-raise the error
immediately.
Failing test case was provided by @rhenium in #394.
Co-authored-by: Kazuki Yamaguchi <k@rhe.jp>
---
lib/net/imap.rb | 10 +++++++++-
test/net/imap/test_imap.rb | 15 +++++++++++++++
2 files changed, 24 insertions(+), 1 deletion(-)
diff --git a/lib/net/imap.rb b/lib/net/imap.rb
index eedcb4f5cf..7a3cd68e81 100644
--- a/lib/net/imap.rb
+++ b/lib/net/imap.rb
@@ -376,7 +376,8 @@ def logout
# Sends a STARTTLS command to start TLS session.
def starttls(options = {}, verify = true)
- send_command("STARTTLS") do |resp|
+ error = nil
+ ok = send_command("STARTTLS") do |resp|
if resp.kind_of?(TaggedResponse) && resp.name == "OK"
begin
# for backward compatibility
@@ -386,7 +387,14 @@ def starttls(options = {}, verify = true)
end
start_tls_session(options)
end
+ rescue Exception => error
+ raise # note that the error backtrace is in the receiver_thread
end
+ if error
+ disconnect
+ raise error
+ end
+ ok
end
# Sends an AUTHENTICATE command to authenticate the client.
diff --git a/test/net/imap/test_imap.rb b/test/net/imap/test_imap.rb
index 2a9447c0e1..b35b3ff52e 100644
--- a/test/net/imap/test_imap.rb
+++ b/test/net/imap/test_imap.rb
@@ -113,6 +113,20 @@ def test_imaps_post_connection_check
end
if defined?(OpenSSL::SSL)
+ def test_starttls_unknown_ca
+ imap = nil
+ ex = nil
+ starttls_test do |port|
+ imap = Net::IMAP.new("localhost", port: port)
+ begin
+ imap.starttls
+ rescue => ex
+ end
+ imap
+ end
+ assert_kind_of(OpenSSL::SSL::SSLError, ex)
+ end
+
def test_starttls
imap = nil
starttls_test do |port|
@@ -765,6 +779,7 @@ def starttls_test
sock.gets
sock.print("* BYE terminating connection\r\n")
sock.print("RUBY0002 OK LOGOUT completed\r\n")
+ rescue OpenSSL::SSL::SSLError
ensure
sock.close
server.close
From 28d2087a097aab75b2d41fddc6dd457a94f657a2 Mon Sep 17 00:00:00 2001
From: nick evans <nicholas.evans@gmail.com>
Date: Tue, 14 Feb 2023 00:13:11 -0500
Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=8D=92=20pick=20609acd9fa:=20?=
=?UTF-8?q?=F0=9F=A5=85=20Add=20new=20`InvalidResponseError`=20exception?=
=?UTF-8?q?=20class=20[backport=20#198]?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This copies the InvalidResponseError from #198 and the rdoc update to
UnknownResponseError. The behavioral changes from that PR have _not_
been copied.
===
The UnknownResponseError is brought into Ruby 2.5 by
ruby-2.6.8-net-imap-startls-stripping-vulnerability.patch,
The errors.rb file does not exist for net-imap on Ruby 2.5, so move the
InvalidResponseError into approximately similar location to be used by
follow-up commits.
---
lib/net/imap.rb | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
diff --git a/lib/net/imap.rb b/lib/net/imap.rb
index 7a3cd68e81..fee094c46d 100644
--- a/lib/net/imap.rb
+++ b/lib/net/imap.rb
@@ -3724,7 +3724,27 @@ class BadResponseError < ResponseError
class ByeResponseError < ResponseError
end
+ # Error raised when the server sends an invalid response.
+ #
+ # This is different from UnknownResponseError: the response has been
+ # rejected. Although it may be parsable, the server is forbidden from
+ # sending it in the current context. The client should automatically
+ # disconnect, abruptly (without logout).
+ #
+ # Note that InvalidResponseError does not inherit from ResponseError: it
+ # can be raised before the response is fully parsed. A related
+ # ResponseParseError or ResponseError may be the #cause.
+ class InvalidResponseError < Error
+ end
+
# Error raised upon an unknown response from the server.
+ #
+ # This is different from InvalidResponseError: the response may be a
+ # valid extension response and the server may be allowed to send it in
+ # this context, but Net::IMAP either does not know how to parse it or
+ # how to handle it. This could result from enabling unknown or
+ # unhandled extensions. The connection may still be usable,
+ # but—depending on context—it may be prudent to disconnect.
class UnknownResponseError < ResponseError
end
From 9f4030066f7ede86b986ef87ed1610f242edc4ca Mon Sep 17 00:00:00 2001
From: nick evans <nick@rubinick.dev>
Date: Fri, 27 Mar 2026 17:16:25 -0400
Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=8D=92=20pick=2046636cae8:=20?=
=?UTF-8?q?=E2=9D=8C=F0=9F=94=92=20Add=20failing=20test=20for=20STARTTLS?=
=?UTF-8?q?=20stripping=20[backport=20#664]?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
I'm putting this in its own commit to simplify testing across backports.
Also, I'm taking a "belt-and-suspenders" approach, and I'm going to test
that either of the two fixes passes the tests.
---
test/net/imap/test_imap.rb | 34 ++++++++++++++++++++++++++++++++++
1 file changed, 34 insertions(+)
diff --git a/test/net/imap/test_imap.rb b/test/net/imap/test_imap.rb
index b35b3ff52e..33606f400b 100644
--- a/test/net/imap/test_imap.rb
+++ b/test/net/imap/test_imap.rb
@@ -151,6 +151,40 @@ def test_starttls_stripping
imap
end
end
+
+ def test_starttls_stripping_ok_sent_before_response
+ # to coordinate between threads (better than sleep)
+ server_to_client, client_to_server = Queue.new, Queue.new
+ imap = nil
+ server = create_tcp_server
+ port = server.addr[1]
+ start_server do
+ sock = server.accept
+ begin
+ sock.print("* OK test server\r\n")
+ assert_equal :send_malicious_response, client_to_server.pop
+ sock.print("RUBY0001 OK hahaha, fooled you!\r\n")
+ server_to_client << :malicious_response_sent
+ sock.gets
+ ensure
+ sock.close
+ server.close
+ end
+ end
+ begin
+ imap = Net::IMAP.new("localhost", :port => port)
+ client_to_server << :send_malicious_response
+ assert_equal :malicious_response_sent, server_to_client.pop
+ sleep 0.010 # to be sure the network buffers have flushed, etc
+ assert_raise(Net::IMAP::InvalidResponseError) do
+ imap.starttls(:ca_file => CA_FILE)
+ end
+ assert imap.disconnected?
+ ensure
+ imap.disconnect if imap && !imap.disconnected?
+ end
+ assert imap.disconnected?
+ end
end
def start_server
From eff739307c73c371769769ca183a0e66baac1e2b Mon Sep 17 00:00:00 2001
From: nick evans <nick@rubinick.dev>
Date: Fri, 27 Mar 2026 17:31:11 -0400
Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=8D=92=20pick=2062eea6ffe:=20?=
=?UTF-8?q?=F0=9F=94=92=F0=9F=A5=85=20Ensure=20STARTTLS=20tagged=20respons?=
=?UTF-8?q?e=20was=20handled=20[backport=20#664]?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Taking a "belt-and-suspenders" approach to a STARTTLS stripping attack:
This handles `STARTTLS` as a special-case: if the `STARTTLS` handler
did not run, for _whatever_ reason, an exception _must_ be raised and
the connection dropped.
_No_ command should ever receive a tagged `OK` prior to completely
sending the command. But `STARTTLS` is security-sensitive enough to
warrant this special-case handler.
---
lib/net/imap.rb | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/lib/net/imap.rb b/lib/net/imap.rb
index fee094c46d..e57f96a464 100644
--- a/lib/net/imap.rb
+++ b/lib/net/imap.rb
@@ -376,9 +376,11 @@ def logout
# Sends a STARTTLS command to start TLS session.
def starttls(options = {}, verify = true)
+ handled = false
error = nil
ok = send_command("STARTTLS") do |resp|
if resp.kind_of?(TaggedResponse) && resp.name == "OK"
+ handled = true
begin
# for backward compatibility
certs = options.to_str
@@ -394,6 +396,13 @@ def starttls(options = {}, verify = true)
disconnect
raise error
end
+ unless handled
+ disconnect
+ raise InvalidResponseError,
+ "STARTTLS handler was bypassed, although server responded %p" % [
+ ok.raw_data.chomp
+ ]
+ end
ok
end
From 2cbdd94ad824721797a17436f60027fb781e8bac Mon Sep 17 00:00:00 2001
From: nick evans <nick@rubinick.dev>
Date: Fri, 27 Mar 2026 18:00:09 -0400
Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=8D=92=20pick=2024d5c773d:=20?=
=?UTF-8?q?=F0=9F=94=92=F0=9F=A5=85=20Handle=20tagged=20"OK"=20to=20incomp?=
=?UTF-8?q?lete=20command=20[backport=20#664]?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Taking a "belt-and-suspenders" approach:
This is a potential problem for any command which registers a response
handler: a malicious server can easily guess what the next tag will be,
and send an `OK` response _before_ the client the response handler is
attached.
`STARTTLS` is an extreme example of this issue: if the `STARTTLS`
handler does not run, then `#starttls` will not start the TLS session,
and the connection is not secured, _but no error is raised._
We should _also_ attach the response handler before sending the `CRLF`,
but that is neither necessary (the response handler will added before
the `synchronize` mutex is unlocked) nor sufficient (the fake `OK` can
be sent _much_ earlier).
On the other hand, it _is_ okay for the server to send an error tagged
response (`NO` or `BAD`), before the sending the command has completed.
---
lib/net/imap.rb | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/lib/net/imap.rb b/lib/net/imap.rb
index e57f96a464..692d6e297a 100644
--- a/lib/net/imap.rb
+++ b/lib/net/imap.rb
@@ -1279,6 +1279,7 @@ def send_command(cmd, *args, &block)
put_string(" ")
send_data(i, tag)
end
+ guard_against_tagged_response_skipping_handler!(tag)
put_string(CRLF)
if cmd == "LOGOUT"
@logout_command_tag = tag
@@ -1294,6 +1295,17 @@ def send_command(cmd, *args, &block)
end
end
end
+ rescue InvalidResponseError
+ disconnect
+ raise
+ end
+
+ def guard_against_tagged_response_skipping_handler!(tag)
+ return unless (resp = @tagged_responses[tag])&.name&.upcase == "OK"
+ raise(InvalidResponseError,
+ "Server sent tagged 'OK' before command was finished: %p. " \
+ "This could indicate a malicious server or client-side " \
+ "command injection. Disconnecting." % [resp.raw_data.chomp])
end
def generate_tag