Fix information disclosure via MITM attack bypassing TLS in net-imap.
Fixes CVE-2026-42246. Backported from https://github.com/ruby/net-imap/commit/e3a6ba4 https://github.com/ruby/net-imap/commit/2068d46 https://github.com/ruby/net-imap/commit/f96ab6c https://github.com/ruby/net-imap/commit/d16e994 https://github.com/ruby/net-imap/commit/6b2fda5 Resolves: RHEL-181777
This commit is contained in:
parent
cfc70379c4
commit
1087f0b172
15
ruby.spec
15
ruby.spec
@ -22,7 +22,7 @@
|
|||||||
%endif
|
%endif
|
||||||
|
|
||||||
|
|
||||||
%global release 166
|
%global release 167
|
||||||
%{!?release_string:%define release_string %{?development_release:0.}%{release}%{?development_release:.%{development_release}}%{?dist}}
|
%{!?release_string:%define release_string %{?development_release:0.}%{release}%{?development_release:.%{development_release}}%{?dist}}
|
||||||
|
|
||||||
# The RubyGems library has to stay out of Ruby directory tree, since the
|
# The RubyGems library has to stay out of Ruby directory tree, since the
|
||||||
@ -347,6 +347,13 @@ Patch80: ruby-3.4.0-ruby-net-http-test_https.rb-fix-test_session_reuse_but_expir
|
|||||||
# Fix arbitrary code execution via deserialization bypass in ERB. (CVE-2026-41316)
|
# Fix arbitrary code execution via deserialization bypass in ERB. (CVE-2026-41316)
|
||||||
# https://github.com/ruby/erb/commit/ef61b591b270f8ba58d47f12472a1c53a77b4d61
|
# https://github.com/ruby/erb/commit/ef61b591b270f8ba58d47f12472a1c53a77b4d61
|
||||||
Patch81: rubygem-erb-4.0.3.1-Fix-arbitrary-code-execution-via-deserialization-bypass-CVE-2026-41316.patch
|
Patch81: rubygem-erb-4.0.3.1-Fix-arbitrary-code-execution-via-deserialization-bypass-CVE-2026-41316.patch
|
||||||
|
# Fix information disclosure via man in the middle attack bypassing TLS. (CVE-2026-42246)
|
||||||
|
# https://github.com/ruby/net-imap/commit/e3a6ba4
|
||||||
|
# https://github.com/ruby/net-imap/commit/2068d46
|
||||||
|
# https://github.com/ruby/net-imap/commit/f96ab6c
|
||||||
|
# https://github.com/ruby/net-imap/commit/d16e994
|
||||||
|
# https://github.com/ruby/net-imap/commit/6b2fda5
|
||||||
|
Patch82: 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}
|
Requires: %{name}-libs%{?_isa} = %{version}-%{release}
|
||||||
Suggests: rubypick
|
Suggests: rubypick
|
||||||
@ -830,6 +837,7 @@ rm -rf ext/fiddle/libffi*
|
|||||||
%patch79 -p1
|
%patch79 -p1
|
||||||
%patch80 -p1
|
%patch80 -p1
|
||||||
%patch81 -p1
|
%patch81 -p1
|
||||||
|
%patch82 -p1
|
||||||
|
|
||||||
# Instead of adjusting patch's directory, use the following form where
|
# Instead of adjusting patch's directory, use the following form where
|
||||||
# we first enter the correct directory, this allows more general application
|
# we first enter the correct directory, this allows more general application
|
||||||
@ -1612,6 +1620,11 @@ make runruby TESTRUN_SCRIPT=" \
|
|||||||
|
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
|
* Thu Jun 11 2026 Jarek Prokop <jprokop@redhat.com> - 3.0.7-167
|
||||||
|
- Fix information disclosure via MITM attack bypassing TLS in net-imap.
|
||||||
|
(CVE-2026-42246)
|
||||||
|
Resolves: RHEL-181777
|
||||||
|
|
||||||
* Mon Apr 27 2026 Jarek Prokop <jprokop@redhat.com> - 3.0.7-166
|
* Mon Apr 27 2026 Jarek Prokop <jprokop@redhat.com> - 3.0.7-166
|
||||||
- Fix arbitrary code execution via deserialization bypass in ERB. (CVE-2026-41316)
|
- Fix arbitrary code execution via deserialization bypass in ERB. (CVE-2026-41316)
|
||||||
Resolves: RHEL-171256
|
Resolves: RHEL-171256
|
||||||
|
|||||||
@ -0,0 +1,324 @@
|
|||||||
|
From 5cf70183a4be110336d45fd955d456667b0d776f 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 d45304f289..e3eddc69d5 100644
|
||||||
|
--- a/lib/net/imap.rb
|
||||||
|
+++ b/lib/net/imap.rb
|
||||||
|
@@ -378,7 +378,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
|
||||||
|
@@ -388,7 +389,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 85fb71d440..7f05db2ecb 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|
|
||||||
|
@@ -831,6 +845,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 09e762a1c7bdfcd1e1964d00328fdd2719127147 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.
|
||||||
|
---
|
||||||
|
lib/net/imap.rb | 20 ++++++++++++++++++++
|
||||||
|
1 file changed, 20 insertions(+)
|
||||||
|
|
||||||
|
diff --git a/lib/net/imap.rb b/lib/net/imap.rb
|
||||||
|
index e3eddc69d5..a33a054e89 100644
|
||||||
|
--- a/lib/net/imap.rb
|
||||||
|
+++ b/lib/net/imap.rb
|
||||||
|
@@ -3729,7 +3729,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 88cc24d757c46677cf029f655d95c22336b084cd 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 7f05db2ecb..a109e289f3 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 f2a8c666cbfa7ef67b1d09de923d7dc74338d7a5 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 a33a054e89..c45c4612d6 100644
|
||||||
|
--- a/lib/net/imap.rb
|
||||||
|
+++ b/lib/net/imap.rb
|
||||||
|
@@ -378,9 +378,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
|
||||||
|
@@ -396,6 +398,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 577bc7ddb780a10e1ba44f42404f14a24e541b01 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 c45c4612d6..1a2afc0ea7 100644
|
||||||
|
--- a/lib/net/imap.rb
|
||||||
|
+++ b/lib/net/imap.rb
|
||||||
|
@@ -1284,6 +1284,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
|
||||||
|
@@ -1299,6 +1300,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
|
||||||
Loading…
Reference in New Issue
Block a user