High severity7.6GHSA Advisory· Published May 9, 2026· Updated May 13, 2026
CVE-2026-42246
CVE-2026-42246
Description
Net::IMAP implements Internet Message Access Protocol (IMAP) client functionality in Ruby. Prior to versions 0.3.10, 0.4.24, 0.5.14, and 0.6.4, a man-in-the-middle attacker can cause Net::IMAP#starttls to return "successfully", without starting TLS. This issue has been patched in versions 0.3.10, 0.4.24, 0.5.14, and 0.6.4.
Affected products
1Patches
424a4e770b432🔀 Merge pull request #666 from ruby/backport/v0.4/STARTTLS-stripping
2 files changed · +58 −0
lib/net/imap.rb+21 −0 modified@@ -1312,9 +1312,11 @@ def logout! # def starttls(**options) @ssl_ctx_params, @ssl_ctx = build_ssl_ctx(options) + handled = false error = nil ok = send_command("STARTTLS") do |resp| if resp.kind_of?(TaggedResponse) && resp.name == "OK" + handled = true clear_cached_capabilities clear_responses start_tls_session @@ -1326,6 +1328,13 @@ def starttls(**options) disconnect raise error end + unless handled + disconnect + raise InvalidResponseError, + "STARTTLS handler was bypassed, although server responded %p" % [ + ok.raw_data.chomp + ] + end ok end @@ -3052,6 +3061,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 @@ -3067,6 +3077,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
test/net/imap/test_imap.rb+37 −0 modified@@ -158,6 +158,43 @@ def test_starttls_stripping assert_equal(CA_FILE, imap.ssl_ctx.ca_file) assert_equal(OpenSSL::SSL::VERIFY_PEER, imap.ssl_ctx.verify_mode) 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_equal false, imap.tls_verified? + assert_equal({ca_file: CA_FILE}, imap.ssl_ctx_params) + assert_equal(CA_FILE, imap.ssl_ctx.ca_file) + assert_equal(OpenSSL::SSL::VERIFY_PEER, imap.ssl_ctx.verify_mode) + end end def start_server
97e2488fb540🔀 Merge pull request #667 from ruby/backport/v0.3/STARTTLS-stripping
4 files changed · +100 −2
Gemfile+1 −1 modified@@ -5,5 +5,5 @@ source "https://rubygems.org" gemspec gem "rake" -gem "rdoc" +gem "rdoc", "<7.2" # incompatible with ruby 2.6 gem "test-unit"
lib/net/imap/errors.rb+20 −0 modified@@ -81,7 +81,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
lib/net/imap.rb+30 −1 modified@@ -1014,8 +1014,11 @@ def logout # unsolicited untagged response immeditely _after_ #starttls completes. # def starttls(options = {}, verify = true) - send_command("STARTTLS") do |resp| + 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 @@ -1024,7 +1027,21 @@ 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 + unless handled + disconnect + raise InvalidResponseError, + "STARTTLS handler was bypassed, although server responded %p" % [ + ok.raw_data.chomp + ] + end + ok end # :call-seq: @@ -2294,6 +2311,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 @@ -2309,6 +2327,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
test/net/imap/test_imap.rb+49 −0 modified@@ -75,6 +75,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| @@ -99,6 +113,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 @@ -1004,6 +1052,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
f79d35bf5833🔀 Merge pull request #665 from ruby/backport/v0.5/STARTTLS-stripping
2 files changed · +61 −0
lib/net/imap.rb+24 −0 modified@@ -1412,9 +1412,11 @@ def logout! # def starttls(**options) @ssl_ctx_params, @ssl_ctx = build_ssl_ctx(options) + handled = false error = nil ok = send_command("STARTTLS") do |resp| if resp.kind_of?(TaggedResponse) && resp.name == "OK" + handled = true clear_cached_capabilities clear_responses start_tls_session @@ -1426,6 +1428,13 @@ def starttls(**options) disconnect raise error end + unless handled + disconnect + raise InvalidResponseError, + "STARTTLS handler was bypassed, although server responded %p" % [ + ok.raw_data.chomp + ] + end ok end @@ -3132,6 +3141,7 @@ def idle(timeout = nil, &response_handler) synchronize do tag = Thread.current[:net_imap_tag] = generate_tag + guard_against_tagged_response_skipping_handler!(tag, "IDLE") put_string("#{tag} IDLE#{CRLF}") begin @@ -3596,6 +3606,7 @@ def send_command(cmd, *args, &block) put_string(" ") send_data(i, tag) end + guard_against_tagged_response_skipping_handler!(tag, cmd) put_string(CRLF) if cmd == "LOGOUT" @logout_command_tag = tag @@ -3611,6 +3622,19 @@ def send_command(cmd, *args, &block) end end end + rescue InvalidResponseError + disconnect + raise + end + + def guard_against_tagged_response_skipping_handler!(tag, cmd) + return unless (resp = @tagged_responses[tag])&.name&.upcase == "OK" + raise InvalidResponseError, format( + "Received tagged 'OK' to incomplete %s command (tag=%s). " \ + "This could indicate a malicious server, a man-in-the-middle, or " \ + "client-side command injection. Disconnecting.", + cmd, tag + ) end def generate_tag
test/net/imap/test_imap.rb+37 −0 modified@@ -153,6 +153,43 @@ def test_starttls_stripping assert_equal(CA_FILE, imap.ssl_ctx.ca_file) assert_equal(OpenSSL::SSL::VERIFY_PEER, imap.ssl_ctx.verify_mode) 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_equal false, imap.tls_verified? + assert_equal({ca_file: CA_FILE}, imap.ssl_ctx_params) + assert_equal(CA_FILE, imap.ssl_ctx.ca_file) + assert_equal(OpenSSL::SSL::VERIFY_PEER, imap.ssl_ctx.verify_mode) + end end def start_server
0ede4c40b152🔀 Merge pull request #664 from ruby/security/STARTTLS-stripping
2 files changed · +84 −35
lib/net/imap.rb+29 −11 modified@@ -1440,9 +1440,11 @@ def logout! # def starttls(**options) @ssl_ctx_params, @ssl_ctx = build_ssl_ctx(options) + handled = false error = nil ok = send_command("STARTTLS") do |resp| if resp.kind_of?(TaggedResponse) && resp.name == "OK" + handled = true clear_cached_capabilities clear_responses start_tls_session @@ -1454,6 +1456,13 @@ def starttls(**options) disconnect raise error end + unless handled + disconnect + raise InvalidResponseError, + "STARTTLS handler was bypassed, although server responded %p" % [ + ok.raw_data.chomp + ] + end ok end @@ -3165,6 +3174,7 @@ def idle(timeout = nil, &response_handler) synchronize do tag = Thread.current[:net_imap_tag] = generate_tag + guard_against_tagged_response_skipping_handler!(tag, "IDLE") put_string("#{tag} IDLE#{CRLF}") begin @@ -3629,21 +3639,29 @@ def send_command(cmd, *args, &block) put_string(" ") send_data(i, tag) end - put_string(CRLF) - if cmd == "LOGOUT" - @logout_command_tag = tag - end - if block - add_response_handler(&block) - end + @logout_command_tag = tag if cmd == "LOGOUT" + guard_against_tagged_response_skipping_handler!(tag, cmd) + add_response_handler(&block) if block begin - return get_tagged_response(tag, cmd) + put_string(CRLF) + get_tagged_response(tag, cmd) ensure - if block - remove_response_handler(block) - end + remove_response_handler(block) if block end end + rescue InvalidResponseError + disconnect + raise + end + + def guard_against_tagged_response_skipping_handler!(tag, cmd) + return unless (resp = @tagged_responses[tag])&.name&.upcase == "OK" + raise InvalidResponseError, format( + "Received tagged 'OK' to incomplete %s command (tag=%s). " \ + "This could indicate a malicious server, a man-in-the-middle, or " \ + "client-side command injection. Disconnecting.", + cmd, tag + ) end def generate_tag
test/net/imap/test_imap.rb+55 −24 modified@@ -138,15 +138,67 @@ def test_starttls end end - def test_starttls_stripping + def test_starttls_stripping_not_ok imap = nil - starttls_stripping_test do |port| + server = create_tcp_server + port = server.addr[1] + start_server do + sock = server.accept + begin + sock.print("* OK test server\r\n") + sock.gets + sock.print("RUBY0001 BUG unhandled command\r\n") + ensure + sock.close + server.close + end + end + begin imap = Net::IMAP.new("localhost", :port => port) assert_raise(Net::IMAP::InvalidResponseError) do imap.starttls(:ca_file => CA_FILE) end assert imap.disconnected? - imap + ensure + imap.disconnect if imap && !imap.disconnected? + end + + assert_equal false, imap.tls_verified? + assert_equal({ca_file: CA_FILE}, imap.ssl_ctx_params) + assert_equal(CA_FILE, imap.ssl_ctx.ca_file) + assert_equal(OpenSSL::SSL::VERIFY_PEER, imap.ssl_ctx.verify_mode) + 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_equal false, imap.tls_verified? assert_equal({ca_file: CA_FILE}, imap.ssl_ctx_params) @@ -963,27 +1015,6 @@ def starttls_test end end - def starttls_stripping_test - server = create_tcp_server - port = server.addr[1] - start_server do - sock = server.accept - begin - sock.print("* OK test server\r\n") - sock.gets - sock.print("RUBY0001 BUG unhandled command\r\n") - ensure - sock.close - server.close - end - end - begin - imap = yield(port) - ensure - imap.disconnect if imap && !imap.disconnected? - end - end - def create_tcp_server return TCPServer.new(server_addr, 0) end
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
14- github.com/advisories/GHSA-vcgp-9326-pqcpghsaADVISORY
- github.com/ruby/net-imap/commit/0ede4c40b1523dfeaf95777b2678e54cc0fd9618nvd
- github.com/ruby/net-imap/commit/24a4e770b43230286a05aa2a9746cdbb3eb8485envd
- github.com/ruby/net-imap/commit/97e2488fb5401a1783bddd959dde007d9fbce42cnvd
- github.com/ruby/net-imap/commit/f79d35bf5833f186e81044c57c843eda30c873danvd
- github.com/ruby/net-imap/releases/tag/v0.3.10nvd
- github.com/ruby/net-imap/releases/tag/v0.4.24nvd
- github.com/ruby/net-imap/releases/tag/v0.5.14nvd
- github.com/ruby/net-imap/releases/tag/v0.6.4ghsa
- github.com/ruby/net-imap/security/advisories/GHSA-vcgp-9326-pqcpnvd
- github.com/rubysec/ruby-advisory-db/blob/master/gems/net-imap/CVE-2026-42246.ymlghsa
- nostarttls.secvuln.infoghsa
- nvd.nist.gov/vuln/detail/CVE-2026-42246ghsa
- www.rfc-editor.org/info/rfc8314ghsa
News mentions
1- Patch Tuesday - May 2026Rapid7 Blog · May 13, 2026