VYPR
Low severity2.1NVD Advisory· Published Jun 9, 2026· Updated Jun 9, 2026

Net::IMAP: Denial of Service via incomplete raw argument validation

CVE-2026-47241

Description

Summary

Several Net::IMAP commands accept a raw string argument which is only validated to prevent CRLF injection and then sent verbatim. If this string is derived from user-controlled input, an attacker can force the next command to be absorbed as a continuation of the first command. This will cause the first command to eventually fail, but also prevents it from returning until another command is sent (from another thread). That other command will not return until the connection is closed.

Details

Net::IMAP::RawData was hardened in v0.6.4, v0.5.14, and v0.4.24 to reject string arguments that would smuggle an invalid literal-continuation marker onto the wire (CVE-2026-42257, GHSA-hm49-wcqc-g2xg). But the trailing-marker check uses an incorrect regex which does not match {0} or {0+}, so an attacker-controlled seach criteria or fetch attr string ending in {0} or {0+} passes validation and is sent verbatim. Since these arguments are sent as the last argument in the command, they will be followed by CRLF. Although the CRLF was intended to end the command, the server will interpret it as part of a literal prefix. This consumes the next command the client puts on the socket as additional arguments to the current command.

This affects the following command's arguments: * criteria for #search and #uid_search * search_keys for #sort, #thread, #uid_sort, and #uid_thread * attr for #fetch and #uid_fetch

The command which contained the attacker's raw data will not be able to complete until the _next_ command is issued. If commands are only sent from single thread, the first command will hang until the connection times out (most likely by the server closing the connection).

If a second command is sent _(from another thread)_, this would allow the server to respond to the first command. This combined command _will_ be invalid: * The {0}\r\n literal prohibits other arguments (such as a quoted string) from spanning both commands * It will be sent without the space delimiter which is required between arguments. * The second command's tag will not be a valid argument to any of the vulnerable commands.

So the server _should_ respond to the first command with a BAD response, which will raise a BadResponseError.

But, since the server never saw a second command, the second command will never receive a tagged response and the thread that sent it will hang until the connection is closed.

Impact

This will result in unexpected crashes and timeouts, which could be used to create a simple denial of service attack. This attack will present very similarly to common network issues or server issues which also result in commands hanging or unexpectedly raising exceptions. By itself, this does not allow command injection. But the confusion caused by these errors could lead to other downstream issues, especially in a multi-threaded environment.

Mitigation

Update to a patched version of net-imap which validates that RawData arguments may not end with literal continuation markers. If net-imap cannot be upgraded: * Validate that user input to the affected command arguments does not end with "}". * Use of Timeout or other standard strategies for slow connections and misbehaving servers will also mitigate the effects of this.

_Extra caution is required when issuing commands from multiple threads._ While net-imap does have rudimentary support for issuing commands from multiple threads, the user is responsible for synchronizing that commands are issued in a logically coherent order, and for ensuring that commands are only pipelined when it is safe to do so. Practically, this means that many commands cannot be safely pipelined together, and user code will often need to wait for state changing commands to successfully complete before issuing commands that rely on that state change.

Affected products

1

Patches

7
e33348ccdc63

🍒 pick d6ddd294 (#700): 🐛 Prevent trailing `{0}` in RawData validation

https://github.com/ruby/net-imapnick evansMay 11, 2026Fixed in 0.5.15via ghsa-release-walk
2 files changed · +8 1
  • lib/net/imap/command_data.rb+1 1 modified
    @@ -218,7 +218,7 @@ def send_data(imap, tag) = data.each do _1.send_data(imap, tag) end
     
           def validate
             return unless data.last in RawText(data: text)
    -        if text.rindex(/~?\{[1-9]\d*\+?\}\z/n)
    +        if text.rindex(/\{\d+\+?\}\z/n)
               raise DataFormatError, "RawData cannot end with literal continuation"
             end
           end
    
  • test/net/imap/test_command_data.rb+7 0 modified
    @@ -480,6 +480,13 @@ class RawDataTest < CommandDataTest
           assert_raise(DataFormatError) do RawData.new(data: "~literal+ ~{123+}") end
           raw = RawData.new(data: " {123} ")
           assert_equal [RawText[" {123} "]], raw.data
    +
    +      assert_raise(DataFormatError) do RawData.new(data: "literal {0}") end
    +      assert_raise(DataFormatError) do RawData.new(data: "literal+ {0+}") end
    +      assert_raise(DataFormatError) do RawData.new(data: "~literal ~{0}") end
    +      assert_raise(DataFormatError) do RawData.new(data: "~literal+ ~{0+}") end
    +      raw = RawData.new(data: " {0} ")
    +      assert_equal [RawText[" {0} "]], raw.data
         end
     
         data(
    
15e33348cc63
https://github.com/ruby/net-imapFixed in 0.5.15via ghsa-release-walk
b3ce36b63554

🍒 pick 94c79576 (#677): 📚⚠️ Boost visibility of raw data argument warnings

https://github.com/ruby/net-imapnick evansMay 14, 2026Fixed in 0.5.15via ghsa-release-walk
1 file changed · +20 8
  • lib/net/imap.rb+20 8 modified
    @@ -2209,6 +2209,7 @@ def uid_expunge(uid_set)
         # provided as an array or a string.
         # See {"Argument translation"}[rdoc-ref:#search@Argument+translation]
         # and {"Search criteria"}[rdoc-ref:#search@Search+criteria], below.
    +    # <em>Please note</em> the warning for when +criteria+ is a String.
         #
         # +return+ options control what kind of information is returned about
         # messages matching the search +criteria+.  Specifying +return+ should force
    @@ -2619,7 +2620,8 @@ def search(...)
         # backward compatibility) but adds SearchResult#modseq when the +CONDSTORE+
         # capability has been enabled.
         #
    -    # See #search for documentation of parameters.
    +    # See #search for documentation of parameters.  <em>Please note</em> the
    +    # warning for when +criteria+ is a String.
         #
         # ==== Capabilities
         #
    @@ -2705,7 +2707,8 @@ def fetch(...)
         # {SequenceSet[...]}[rdoc-ref:SequenceSet@Creating+sequence+sets].
         # (For message sequence numbers, use #fetch instead.)
         #
    -    # +attr+ behaves the same as with #fetch.
    +    # +attr+ behaves the same as with #fetch.  <em>Please note</em> the #fetch
    +    # warning on the +attr+ argument.
         # >>>
         #   *Note:* Servers _MUST_ implicitly include the +UID+ message data item as
         #   part of any +FETCH+ response caused by a +UID+ command, regardless of
    @@ -2917,8 +2920,10 @@ def uid_move(set, mailbox)
     
         # Sends a {SORT command [RFC5256 §3]}[https://www.rfc-editor.org/rfc/rfc5256#section-3]
         # to search a mailbox for messages that match +search_keys+ and return an
    -    # array of message sequence numbers, sorted by +sort_keys+.  +search_keys+
    -    # are interpreted the same as for #search.
    +    # array of message sequence numbers, sorted by +sort_keys+.
    +    #
    +    # +search_keys+ are interpreted the same as the +criteria+ argument for
    +    # #search.  <em>Please note</em> the #search warning for String +criteria+.
         #
         #--
         # TODO: describe +sort_keys+
    @@ -2943,8 +2948,10 @@ def sort(sort_keys, search_keys, charset)
     
         # Sends a {UID SORT command [RFC5256 §3]}[https://www.rfc-editor.org/rfc/rfc5256#section-3]
         # to search a mailbox for messages that match +search_keys+ and return an
    -    # array of unique identifiers, sorted by +sort_keys+.  +search_keys+ are
    -    # interpreted the same as for #search.
    +    # array of unique identifiers, sorted by +sort_keys+.
    +    #
    +    # +search_keys+ are interpreted the same as the +criteria+ argument for
    +    # #search.  <em>Please note</em> the #search warning for String +criteria+.
         #
         # Related: #sort, #search, #uid_search, #thread, #uid_thread
         #
    @@ -2958,8 +2965,10 @@ def uid_sort(sort_keys, search_keys, charset)
     
         # Sends a {THREAD command [RFC5256 §3]}[https://www.rfc-editor.org/rfc/rfc5256#section-3]
         # to search a mailbox and return message sequence numbers in threaded
    -    # format, as a ThreadMember tree.  +search_keys+ are interpreted the same as
    -    # for #search.
    +    # format, as a ThreadMember tree.
    +    #
    +    # +search_keys+ are interpreted the same as the +criteria+ argument for
    +    # #search.  <em>Please note</em> the #search warning for String +criteria+.
         #
         # The supported algorithms are:
         #
    @@ -2985,6 +2994,9 @@ def thread(algorithm, search_keys, charset)
         # Similar to #thread, but returns unique identifiers instead of
         # message sequence numbers.
         #
    +    # +search_keys+ are interpreted the same as the +criteria+ argument for
    +    # #search.  <em>Please note</em> the #search warning for String +criteria+.
    +    #
         # Related: #thread, #search, #uid_search, #sort, #uid_sort
         #
         # ==== Capabilities
    
69da4a490dd1

🍒 pick 8d9397ab (#698): 🥅 Validate QuotedString contains only valid bytes

https://github.com/ruby/net-imapnick evansMay 2, 2026Fixed in 0.5.15via ghsa-release-walk
2 files changed · +113 18
  • lib/net/imap/command_data.rb+21 8 modified
    @@ -150,8 +150,8 @@ def validate
           end
         end
     
    -    # Represents IMAP +text+ data, which covers everything in the IMAP grammar,
    -    # except for +literal+, +literal8+, and the concluding +CRLF+.
    +    # Represents IMAP +text+ or +quoted+ data, which share the same
    +    # validations of decoded #data, and differ only in how they are formatted.
         #
         # +data+ may contain any 7-bit ASCII character except +NULL+, +CR+, or +LF+.
         # Any multibyte +UTF-8+ character is also allowed when the connection
    @@ -164,7 +164,7 @@ def validate
         #
         # The string's bytes must be valid ASCII or valid UTF-8.  The string's
         # reported encoding is ignored, but the string is _not_ transcoded.
    -    class RawText < CommandData # :nodoc:
    +    class ValidNonLiteralData < CommandData
           def initialize(data:)
             data = String(data.to_str)
             unless data.encoding in Encoding::ASCII | Encoding::UTF_8
    @@ -189,7 +189,17 @@ def validate
     
           def ascii_only? = data.ascii_only?
     
    -      def send_data(imap, tag) = imap.__send__(:put_string, data)
    +      def send_data(imap, tag = nil) = imap.__send__(:put_string, formatted)
    +    end
    +
    +    # Represents IMAP +text+ data, which covers everything in the IMAP grammar,
    +    # except for +literal+, +literal8+, and the concluding +CRLF+.
    +    #
    +    # NOTE: The current implementation does not verify that the connection
    +    # supports UTF-8.  Future versions may validate this.
    +    class RawText < ValidNonLiteralData # :nodoc:
    +      # raw: no formatting necessary
    +      alias formatted data
         end
     
         class RawData < CommandData # :nodoc:
    @@ -268,10 +278,13 @@ def send_data(imap, tag)
           end
         end
     
    -    class QuotedString < CommandData # :nodoc:
    -      def send_data(imap, tag)
    -        imap.__send__(:send_quoted_string, data)
    -      end
    +    # Represents a IMAP +quoted+ string, which can encode any valid ASCII or
    +    # UTF-8 string, unless it contains any +CR+, +LF+, or +NULL+ bytes.
    +    #
    +    # NOTE: The current implementation does not verify that the connection
    +    # supports UTF-8.  Future versions may validate this.
    +    class QuotedString < ValidNonLiteralData # :nodoc:
    +      def formatted = %("#{data.gsub(/["\\]/, "\\\\\\&")}")
         end
     
         class Literal < Data.define(:data, :non_sync) # :nodoc:
    
  • test/net/imap/test_command_data.rb+92 10 modified
    @@ -8,6 +8,7 @@ class CommandDataTest < Net::IMAP::TestCase
     
       Atom = Net::IMAP::Atom
       Flag = Net::IMAP::Flag
    +  QuotedString = Net::IMAP::QuotedString
       Literal = Net::IMAP::Literal
       Literal8 = Net::IMAP::Literal8
       RawText = Net::IMAP::RawText
    @@ -155,6 +156,83 @@ def send_data(*data, tag: TAG)
         ], imap.output
       end
     
    +  class QuotedStringTest < CommandDataTest
    +    test "quotes ASCII strings (no specials)" do
    +      assert_equal '"INBOX"', QuotedString["INBOX"].formatted
    +      imap.send_data(
    +        QuotedString["INBOX"],
    +        QuotedString["etc"]
    +      )
    +      assert_equal [
    +        Output.put_string('"INBOX"'),
    +        Output.put_string('"etc"'),
    +      ], imap.output
    +      imap.clear
    +    end
    +
    +    test "quotes ASCII strings (atom specials)" do
    +      [
    +        "  with  spaces  in  string  ",
    +        "with_parens()",
    +        "with_list_wildcards*",
    +        "with_list_wildcards%",
    +        "with_resp_special]",
    +        "with\x7fcontrol_char",
    +        %{(){}[]%*'},
    +      ].each do |string|
    +        imap.send_data QuotedString[string]
    +      end
    +      assert_equal [
    +        Output.put_string('"  with  spaces  in  string  "'),
    +        Output.put_string('"with_parens()"'),
    +        Output.put_string('"with_list_wildcards*"'),
    +        Output.put_string('"with_list_wildcards%"'),
    +        Output.put_string('"with_resp_special]"'),
    +        Output.put_string(%{"with\x7fcontrol_char"}),
    +        Output.put_string(%Q{"(){}[]%*'"}),
    +      ], imap.output
    +    end
    +
    +    test "escapes quoted specials" do
    +      [
    +        '"with" "quoted" "specials"',
    +        "\\with\\quoted\\specials\\",
    +        %{(){}[]%*"'\\},
    +      ].each do |string|
    +        imap.send_data QuotedString[string]
    +      end
    +      assert_equal [
    +        Output.put_string('"\"with\" \"quoted\" \"specials\""'),
    +        Output.put_string('"\\\\with\\\\quoted\\\\specials\\\\"'),
    +        Output.put_string(%q{"(){}[]%*\"'\\\\"}),
    +      ], imap.output
    +    end
    +
    +    test "ASCII compatible string with another encodings" do
    +      imap.send_data QuotedString.new("foo bar".encode("cp1252"))
    +      assert_equal [
    +        Output.put_string('"foo bar"'),
    +      ], imap.output
    +    end
    +
    +    test "allows ASCII control chars" do
    +      text = QuotedString.new("beep\b beep\b escape!\e delete this:\x1f")
    +      imap.send_data text
    +      assert_equal [
    +        Output.put_string(%{"beep\b beep\b escape!\e delete this:\x1f"}),
    +      ], imap.output
    +    end
    +
    +    test "quotes valid UTF-8 multibyte chars" do
    +      imap.send_data QuotedString.new("föó bär")
    +      imap.send_data QuotedString.new("ほげ ふが ぴよ")
    +      assert_equal [
    +        Output.put_string('"föó bär"'),
    +        Output.put_string('"ほげ ふが ぴよ"'),
    +      ], imap.output
    +    end
    +  end
    +
       class RawTextTest < CommandDataTest
         test "allows ASCII strings with no specials" do
           imap.send_data(
    @@ -229,14 +307,20 @@ class RawTextTest < CommandDataTest
             Output.put_string("ほげ ふが ぴよ"),
           ], imap.output
         end
    +  end
     
    +  SharedValidNonLiteralDataTests = ->(data_type) do
         data(
           "NULL" => ["with \0 NULL", /NULL\b.+\bbyte/i],
           "CR"   => ["with \r CR",   /CR\b.+\bbyte/i],
           "LF"   => ["with \n LF",   /LF\b.+\bbyte/i],
         )
         test "invalid ASCII byte" do |(text, error_message)|
    -      try_multiple_encodings(error_message, text)
    +      with_multiple_encodings(text) do |encoded|
    +        assert_raise_with_message(DataFormatError, error_message) do
    +          data_type[encoded]
    +        end
    +      end
         end
     
         # See Table 3-7, Well-Formed UTF-8 Byte Sequences, in The Unicode Standard:
    @@ -253,7 +337,11 @@ class RawTextTest < CommandDataTest
           "windows-1252"               => "åêïõü".encode("windows-1252"),
         )
         test "invalid UTF-8" do |text|
    -      try_multiple_encodings(/invalid UTF-8/i, text)
    +      with_multiple_encodings(text) do |encoded|
    +        assert_raise_with_message(DataFormatError, /invalid UTF-8/i) do
    +          data_type[encoded]
    +        end
    +      end
         end
     
         def with_multiple_encodings(data)
    @@ -262,15 +350,9 @@ def with_multiple_encodings(data)
           yield data.dup.force_encoding("UTF-8")
           yield data.dup.force_encoding("cp1252")
         end
    -
    -    def try_multiple_encodings(error_message, data)
    -      with_multiple_encodings(data) do |encoded|
    -        assert_raise_with_message(DataFormatError, error_message) do
    -          RawText[encoded]
    -        end
    -      end
    -    end
       end
    +  QuotedStringTest.class_exec QuotedString, &SharedValidNonLiteralDataTests
    +  RawTextTest     .class_exec RawText,      &SharedValidNonLiteralDataTests
     
       class RawDataTest < CommandDataTest
         test "simple raw text" do
    
b437552d0029

🍒 pick 95afda8f (#679): ♻️ Allow RawData.new to directly set parts array

https://github.com/ruby/net-imapnick evansMay 1, 2026Fixed in 0.5.15via ghsa-release-walk
1 file changed · +6 1
  • lib/net/imap/command_data.rb+6 1 modified
    @@ -192,7 +192,12 @@ def send_data(imap, tag) = imap.__send__(:put_string, data)
     
         class RawData < CommandData # :nodoc:
           def initialize(data:)
    -        data = split_parts(data)
    +        case data
    +        in String then data = split_parts(data)
    +        in Array  if   data.all? { _1 in RawText | Literal }
    +        else
    +          raise TypeError, "expected String or Array[#{RawText} | #{Literal}]"
    +        end
             super
             validate
           end
    
15bd0c4a861a
https://github.com/ruby/net-imapFixed in 0.5.15via ghsa-release-walk
b3ce36b63554
https://github.com/ruby/net-imapFixed in 0.5.15via ghsa-release-walk

Vulnerability mechanics

No source-code context for this CVE — mechanics is only generated when we can read the actual fix diff. Without that, the four sections (root cause, attack vector, affected code, fix) would be speculation rather than analysis.

References

3

News mentions

0

No linked articles in our index yet.