ruby/SOURCES/rubygem-net-imap-0.4.24-Fix-IMAP-Command-Injection-via-Symbol-Arguments-CVE-2026-42258.patch
2026-07-02 15:28:27 -04:00

243 lines
7.2 KiB
Diff

From 20814a27978725dfcbce7f1b149d4cb44e634653 Mon Sep 17 00:00:00 2001
From: nick evans <nick@rubinick.dev>
Date: Wed, 22 Apr 2026 11:25:06 -0400
Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=8D=92=20edit=20ca72ac45:=20=E2=99=BB?=
=?UTF-8?q?=EF=B8=8F=20Extract=20superclass=20for=20(internal)=20command?=
=?UTF-8?q?=20data?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Unlike the cherry-picked commit (ca72ac45), this _only_ makes
`CommandData` the superclass for `Literal` and `Atom`. Because those
are the classes that will be modified by later cherry-picked commits.
This allows those other commits to merge more cleanly, and work with
fewer modifications.
---
lib/net/imap.rb | 44 ++++++++++++++++++++++++++++----------------
1 file changed, 28 insertions(+), 16 deletions(-)
diff --git a/lib/net/imap.rb b/lib/net/imap.rb
index 692d6e297a..ba44d21956 100644
--- a/lib/net/imap.rb
+++ b/lib/net/imap.rb
@@ -1566,22 +1566,43 @@ def start_tls_session(params = {})
end
end
- class RawData # :nodoc:
+ # simplistic emulation of CommandData = Data.define(:data)
+ class CommandData
+ class << self
+ def new(arg = nil, data: arg) super(data: data) end
+ alias :[] :new
+ end
+
+ def initialize(data:)
+ @data = data
+ freeze
+ end
+
+ attr_reader :data
+
+ def to_h(&block) block ? to_h.to_h(&block) : { data: data } end
+ def ==(other) self.class === other && to_h == other.to_h end
+ def eql?(other) self.class === other && to_h.eql?(other.to_h) end
+
+ # following class definition goes beyond the basic Data.define(:data)
+ ##
+
def send_data(imap, tag)
- imap.send(:put_string, @data)
+ raise NoMethodError, "#{self.class} must implement #{__method__}"
end
def validate
end
+ end
- private
- def initialize(data)
- @data = data
+ class RawData < CommandData # :nodoc:
+ def send_data(imap, tag)
+ imap.send(:put_string, @data)
end
end
- class Atom # :nodoc:
+ class Atom < CommandData # :nodoc:
def send_data(imap, tag)
imap.send(:put_string, @data)
end
@@ -1611,19 +1632,10 @@ def initialize(data)
end
end
- class Literal # :nodoc:
+ class Literal < CommandData # :nodoc:
def send_data(imap, tag)
imap.send(:send_literal, @data, tag)
end
-
- def validate
- end
-
- private
-
- def initialize(data)
- @data = data
- end
end
class MessageSet # :nodoc:
From 5c6f3ce364478fd8fde9fe8c9fc47cad34ac4ba7 Mon Sep 17 00:00:00 2001
From: nick evans <nick@rubinick.dev>
Date: Thu, 19 Feb 2026 15:06:08 -0500
Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=8D=92=20pick=209db3e9d60:=20?=
=?UTF-8?q?=F0=9F=A5=85=20Strictly=20validate=20symbol=20(\flag)=20argumen?=
=?UTF-8?q?ts=20[backports=20#657]?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Flags should not allow `atom-specials`.
Previously, no validation was done on symbol data. Sending atom or flag
args which contain atom specials could lead to various errors.
Although this could theoretically include injection attacks, this is not
considered to be a critical vulnerability in `net-imap`, for the
following reason: Valid "system flag" inputs are restricted to an
enumerated set of RFC-defined flag types. User-defined "keyword" flags
are sent as atoms, not flags, which use string inputs (strings which
can't be sent as an atom will be quoted or sent as a literal). `\Seen`
as a flag (symbol argument) is semantically different from `Seen` as a
keyword (string argument). So there is no scenario where it is
appropriate to call `#to_sym` on unvetted user input. Any code which
calls `#to_sym` indiscriminately on user-input is already buggy.
Nevertheless, users should reasonably be able to rely on `net-imap` to
do very basic input validation on its basic input types.
===
Tests were not backported, they use a much newer style that does not
make sense to backport, when we can run the tests elsewhere instead of
trying to essentially write some portions anew.
---
lib/net/imap.rb | 32 ++++++++++++++++++++++----------
1 file changed, 22 insertions(+), 10 deletions(-)
diff --git a/lib/net/imap.rb b/lib/net/imap.rb
index ba44d21956..4354731f13 100644
--- a/lib/net/imap.rb
+++ b/lib/net/imap.rb
@@ -1344,6 +1344,7 @@ def validate_data(data)
end
when Time
when Symbol
+ Flag.validate(data)
else
data.validate
end
@@ -1362,7 +1363,7 @@ def send_data(data, tag = nil)
when Time
send_time_data(data)
when Symbol
- send_symbol_data(data)
+ Flag[data].send_data(self, tag)
else
data.send_data(self, tag)
end
@@ -1432,10 +1433,6 @@ def send_time_data(time)
put_string(s)
end
- def send_symbol_data(symbol)
- put_string("\\" + symbol.to_s)
- end
-
def search_internal(cmd, keys, charset)
if keys.instance_of?(String)
keys = [RawData.new(keys)]
@@ -1587,6 +1584,12 @@ def eql?(other) self.class === other && to_h.eql?(other.to_h) end
# following class definition goes beyond the basic Data.define(:data)
##
+ def self.validate(*args)
+ data = new(*args)
+ data.validate
+ data
+ end
+
def send_data(imap, tag)
raise NoMethodError, "#{self.class} must implement #{__method__}"
end
@@ -1603,17 +1606,26 @@ def send_data(imap, tag)
end
class Atom < CommandData # :nodoc:
- def send_data(imap, tag)
- imap.send(:put_string, @data)
+ def initialize(**)
+ super
+ validate
end
def validate
+ data.to_s.ascii_only? \
+ or raise DataFormatError, "#{self.class} must be ASCII only"
+ data.match?(ResponseParser::Patterns::ATOM_SPECIALS) \
+ and raise DataFormatError, "#{self.class} must not contain atom-specials"
end
- private
+ def send_data(imap, tag)
+ imap.send(:put_string, data.to_s)
+ end
+ end
- def initialize(data)
- @data = data
+ class Flag < Atom # :nodoc:
+ def send_data(imap, tag)
+ imap.send(:put_string, "\\#{data}")
end
end
From 1f8a99c432f748dfccb275cc696d8ac58c2255e6 Mon Sep 17 00:00:00 2001
From: Jarek Prokop <jprokop@redhat.com>
Date: Tue, 9 Jun 2026 18:14:51 +0200
Subject: [PATCH 3/3] Cherry-pick ATOM_SPECIALS for validation regex.
The previous commit relies on ResponseParsers::Patterns::ATOM_SPECIALS
to be available. We could either hardcode the regex in the line where
the matching happens, but instead have it live in its own constant for
easier reference and less edits to the actual commit backport.
The line ported from:
https://github.com/ruby/net-imap/commit/92db350b24c388d2a2104f36cac9caa49a1044df
---
lib/net/imap.rb | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/lib/net/imap.rb b/lib/net/imap.rb
index 4354731f13..2af43806bb 100644
--- a/lib/net/imap.rb
+++ b/lib/net/imap.rb
@@ -2218,6 +2218,10 @@ def multipart?
end
class ResponseParser # :nodoc:
+ module Patterns
+ ATOM_SPECIALS = /[(){ \x00-\x1f\x7f%*"\\\]]/n
+ end
+
def initialize
@str = nil
@pos = nil