VYPR
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

1

Patches

4
24a4e770b432

🔀 Merge pull request #666 from ruby/backport/v0.4/STARTTLS-stripping

https://github.com/ruby/net-imapnicholas a. evansApr 23, 2026via ghsa
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

https://github.com/ruby/net-imapnicholas a. evansApr 23, 2026via ghsa
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

https://github.com/ruby/net-imapnicholas a. evansApr 23, 2026via ghsa
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

https://github.com/ruby/net-imapnicholas a. evansApr 23, 2026via ghsa
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

News mentions

1