import UBI ruby-3.3.10-13.el10_2

This commit is contained in:
AlmaLinux RelEng Bot 2026-06-30 12:27:36 -04:00
parent 6a563d917a
commit bac3e18856
4 changed files with 700 additions and 2 deletions

View File

@ -173,7 +173,7 @@
Summary: An interpreter of object-oriented scripting language
Name: ruby
Version: %{ruby_version}%{?development_release}
Release: 12%{?dist}
Release: 13%{?dist}
# Licenses, which are likely not included in binary RPMs:
# Apache-2.0:
# benchmark/gc/redblack.rb
@ -299,6 +299,24 @@ Patch16: ruby-3.4.2-openssl-Fix-SHA-1-PSS-tests.patch
# https://github.com/ruby/ruby/commit/a53f3d57d1c70f35534c457de2c471d84a55956a
# https://github.com/ruby/ruby/commit/2f223e90edf40f5d537760cb26c77b608bddff36
Patch17: rubygem-erb-4.0.3.1-Fix-arbitrary-code-execution-via-deserialization-bypass-CVE-2026-41316.patch
# Test commits from PR were dropped. The gem does not have tests in the ruby tar.
# Backported from
# https://github.com/ruby/net-imap/commit/f46fe38
# https://github.com/ruby/net-imap/commit/0560d26
# https://github.com/ruby/net-imap/commit/bfdae21
Patch18: rubygem-net-imap-0.4.24-DoS-via-crafted-IMAP-responses-CVE-2026-42245.patch
# Test commits from PR were dropped. The gem does not have tests in the ruby tar.
# Backported from
# https://github.com/ruby/net-imap/commit/705aa59
# https://github.com/ruby/net-imap/commit/038ae35
Patch19: rubygem-net-imap-0.4.24-Information-disclosure-via-MITM-attack-bypassing-TLS-2026-42246.patch
# Tests not included, the gem does not have tests in the ruby tar.
# Original source PR#663 but it bunches more fixes together, so we backport less.
# https://github.com/ruby/net-imap/pull/663
# Backported from the following, the first commit below is requirement for the actual fix:
# https://github.com/ruby/net-imap/commit/1eb27278a601be1135910dae6ab6e517559a2e4a
# https://github.com/ruby/net-imap/commit/bbd9eb7ecca506fa43b656368f7aebef8ac09182
Patch20: rubygem-net-imap-0.4.24-Command-Injection-via-Symbol-Arguments-CVE-2026-42258.patch
Requires: %{name}-libs%{?_isa} = %{version}-%{release}
%{?with_rubypick:Suggests: rubypick}
@ -784,6 +802,12 @@ analysis result in RBS format, a standard type description format for Ruby
%patch 16 -p1
%patch 17 -p1
pushd .bundle/gems/net-imap-%{net_imap_version}
%patch 18 -p1
%patch 19 -p1
%patch 20 -p1
popd
# Provide an example of usage of the tapset:
cp -a %{SOURCE3} .
@ -1788,9 +1812,18 @@ make -C %{_vpath_builddir} runruby TESTRUN_SCRIPT=" \
%changelog
* Thu Jun 11 2026 Jarek Prokop <jprokop@redhat.com> - 3.3.10-13
- Fix DoS via crafted IMAP responses in net-imap. (CVE-2026-42245)
Resolves: RHEL-181687
- Fix information disclosure via MITM attack bypassing TLS in net-imap.
(CVE-2026-42246)
Resolves: RHEL-181775
- Fix command injection via Symbol arguments in net-imap. (CVE-2026-42258)
Resolves: RHEL-181797
* Tue Apr 28 2026 Jarek Prokop <jprokop@redhat.com> - 3.3.10-12
- Fix arbitrary code execution via deserialization bypass in ERB. (CVE-2026-41316)
Resolves: RHEL-171244
Resolves: RHEL-171245
* Thu Nov 13 2025 Jun Aruga <jaruga@redhat.com> - 3.3.10-11
- Upgrade to Ruby 3.3.10.

View File

@ -0,0 +1,209 @@
From e55d73b170eb4a5523a411cdc9bd5cf13121694e 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/2] =?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/command_data.rb | 50 +++++++++++++++++++-----------------
1 file changed, 26 insertions(+), 24 deletions(-)
diff --git a/lib/net/imap/command_data.rb b/lib/net/imap/command_data.rb
index 180a254..91c7336 100644
--- a/lib/net/imap/command_data.rb
+++ b/lib/net/imap/command_data.rb
@@ -119,33 +119,44 @@ module Net
put_string("\\" + symbol.to_s)
end
- class RawData # :nodoc:
- def send_data(imap, tag)
- imap.__send__(:put_string, @data)
+ # simplistic emulation of CommandData = Data.define(:data)
+ class CommandData # :nodoc:
+ class << self
+ def new(arg = nil, data: arg) super(data: data) end
+ alias :[] :new
end
- def validate
+ def initialize(data:)
+ @data = data
+ freeze
end
- private
+ attr_reader :data
- def initialize(data)
- @data = data
- end
- end
+ 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)
+ ##
- class Atom # :nodoc:
def send_data(imap, tag)
- imap.__send__(:put_string, @data)
+ raise NoMethodError, "#{self.class} must implement #{__method__}"
end
def validate
end
+ end
- private
+ class RawData < CommandData # :nodoc:
+ def send_data(imap, tag)
+ imap.__send__(:put_string, @data)
+ end
+ end
- def initialize(data)
- @data = data
+ class Atom < CommandData # :nodoc:
+ def send_data(imap, tag)
+ imap.__send__(:put_string, @data)
end
end
@@ -164,19 +175,10 @@ module Net
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 d1c362631589b20e59f5e64fe1ee9222b3ea07f9 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/2] =?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.
---
lib/net/imap/command_data.rb | 33 +++++++++++++++++++++++++++------
1 file changed, 27 insertions(+), 6 deletions(-)
diff --git a/lib/net/imap/command_data.rb b/lib/net/imap/command_data.rb
index 91c7336..499002f 100644
--- a/lib/net/imap/command_data.rb
+++ b/lib/net/imap/command_data.rb
@@ -25,6 +25,7 @@ module Net
end
when Time, Date, DateTime
when Symbol
+ Flag.validate(data)
else
data.validate
end
@@ -45,7 +46,7 @@ module Net
when Date
send_date_data(data)
when Symbol
- send_symbol_data(data)
+ Flag[data].send_data(self, tag)
else
data.send_data(self, tag)
end
@@ -115,10 +116,6 @@ module Net
def send_date_data(date) put_string Net::IMAP.encode_date(date) end
def send_time_data(time) put_string Net::IMAP.encode_time(time) end
- def send_symbol_data(symbol)
- put_string("\\" + symbol.to_s)
- end
-
# simplistic emulation of CommandData = Data.define(:data)
class CommandData # :nodoc:
class << self
@@ -140,6 +137,12 @@ module Net
# following class definition goes beyond the basic Data.define(:data)
##
+ def self.validate(...)
+ data = new(...)
+ data.validate
+ data
+ end
+
def send_data(imap, tag)
raise NoMethodError, "#{self.class} must implement #{__method__}"
end
@@ -155,8 +158,26 @@ module Net
end
class Atom < CommandData # :nodoc:
+ 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
+
def send_data(imap, tag)
- imap.__send__(:put_string, @data)
+ imap.__send__(:put_string, data.to_s)
+ end
+ end
+
+ class Flag < Atom # :nodoc:
+ def send_data(imap, tag)
+ imap.__send__(:put_string, "\\#{data}")
end
end

View File

@ -0,0 +1,340 @@
From cd5ccc6948eac1a0b5d5da625ab19f56df6cfaa5 Mon Sep 17 00:00:00 2001
From: nick evans <nick@rubinick.dev>
Date: Tue, 31 Mar 2026 19:28:20 -0400
Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=8D=92=20pick=20341ab6281:=20?=
=?UTF-8?q?=E2=9A=A1=EF=B8=8F=F0=9F=94=92=EF=B8=8F=20Fix=20non-linear=20pe?=
=?UTF-8?q?rformance=20in=20ResponseReader?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
A very large response with many small repeated literals can trigger
super-linear time. This happens because the regular expression that
checks for literal continuation matches from the beginning of the buffer
every time.
This could be mitigated by searching from an offset, based on what has
already been processed, or only searching the most recent line (before
merging it with the buffer), but that is still `O(n)` on line length.
The regexp is anchored to the end of the string, so searching in reverse
from the end of the string should be `O(1)`. This is accomplished by
converting `=~` to `rindex`.
Note that this _does_ slow down the "no literals" scenario.
```
$ benchmark-driver benchmarks/response_reader.yml --filter KiB
Warming up --------------------------------------
1KiB with no literals 143.564k i/s - 153.197k times in 1.067099s (6.97μs/i)
10KiB with no literals 27.394k i/s - 28.864k times in 1.053670s (36.50μs/i)
100KiB with no literals 2.926k i/s - 3.157k times in 1.079109s (341.81μs/i)
1KiB of 25B literals 2.786k i/s - 2.970k times in 1.066159s (358.98μs/i)
10KiB of 25B literals 263.498 i/s - 286.000 times in 1.085396s (3.80ms/i)
100KiB of 25B literals 19.470 i/s - 20.000 times in 1.027203s (51.36ms/i)
1KiB of 0B literals 530.014 i/s - 530.000 times in 0.999974s (1.89ms/i)
10KiB of 0B literals 45.239 i/s - 50.000 times in 1.105233s (22.10ms/i)
100KiB of 0B literals 3.075 i/s - 4.000 times in 1.300721s (325.18ms/i)
Calculating -------------------------------------
local YJIT
1KiB with no literals 137.049k 159.971k i/s - 430.691k times in 3.142607s 2.692304s
10KiB with no literals 27.272k 28.101k i/s - 82.181k times in 3.013413s 2.924470s
100KiB with no literals 2.941k 2.937k i/s - 8.776k times in 2.984095s 2.988129s
1KiB of 25B literals 2.803k 4.136k i/s - 8.357k times in 2.981249s 2.020772s
10KiB of 25B literals 262.978 385.394 i/s - 790.000 times in 3.004055s 2.049850s
100KiB of 25B literals 18.355 22.549 i/s - 58.000 times in 3.159962s 2.572152s
1KiB of 0B literals 505.733 759.572 i/s - 1.590k times in 3.143953s 2.093285s
10KiB of 0B literals 45.414 67.569 i/s - 135.000 times in 2.972648s 1.997962s
100KiB of 0B literals 2.722 3.510 i/s - 9.000 times in 3.306786s 2.564007s
Comparison:
1KiB with no literals
YJIT: 159971.1 i/s
local: 137049.0 i/s - 1.17x slower
10KiB with no literals
YJIT: 28101.2 i/s
local: 27271.7 i/s - 1.03x slower
100KiB with no literals
local: 2940.9 i/s
YJIT: 2937.0 i/s - 1.00x slower
1KiB of 25B literals
YJIT: 4135.5 i/s
local: 2803.2 i/s - 1.48x slower
10KiB of 25B literals
YJIT: 385.4 i/s
local: 263.0 i/s - 1.47x slower
100KiB of 25B literals
YJIT: 22.5 i/s
local: 18.4 i/s - 1.23x slower
1KiB of 0B literals
YJIT: 759.6 i/s
local: 505.7 i/s - 1.50x slower
10KiB of 0B literals
YJIT: 67.6 i/s
local: 45.4 i/s - 1.49x slower
100KiB of 0B literals
YJIT: 3.5 i/s
local: 2.7 i/s - 1.29x slower
```
For responses that are larger than 10KiB, the benchmarks do take another
dip. Despite that, I believe the algorithm _is_ still linear, and that
the performance hit on large responses is probably due to the large
strings inducing memory locality (paging/caching) bottlenecks.
---
lib/net/imap/response_reader.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/net/imap/response_reader.rb b/lib/net/imap/response_reader.rb
index fd7561f..d59d672 100644
--- a/lib/net/imap/response_reader.rb
+++ b/lib/net/imap/response_reader.rb
@@ -32,7 +32,7 @@ module Net
def empty?; buff.empty? end
def done?; line_done? && !get_literal_size end
def line_done?; buff.end_with?(CRLF) end
- def get_literal_size; /\{(\d+)\}\r\n\z/n =~ buff && $1.to_i end
+ def get_literal_size; buff.rindex(/\{(\d+)\}\r\n\z/n) && $1.to_i end
def read_line
buff << (@sock.gets(CRLF, read_limit) or throw :eof)
From c6493156d14dbac9e46a0b14e7d01ee5b83c70a9 Mon Sep 17 00:00:00 2001
From: nick evans <nick@rubinick.dev>
Date: Wed, 15 Apr 2026 10:00:24 -0400
Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=8D=92=20pick=2049c516d62:=20?=
=?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Faster=20ResponseParser:=20short-circuit?=
=?UTF-8?q?=20no=20literal?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
I was suprised at how much slower `buff.rindex` is (vs `=~`) when it
doesn't match. This speeds up that case significantly (it's now faster
than it was prior to the `rindex` change), with only a small impact in
the case when it does match.
```
$ benchmark-driver benchmarks/response_reader.yml --filter KiB
Warming up --------------------------------------
1KiB with no literals 202.754k i/s - 210.144k times in 1.036449s (4.93μs/i)
10KiB with no literals 55.683k i/s - 57.541k times in 1.033362s (17.96μs/i)
100KiB with no literals 6.654k i/s - 7.176k times in 1.078491s (150.29μs/i)
1KiB of 25B literals 2.780k i/s - 2.959k times in 1.064363s (359.70μs/i)
10KiB of 25B literals 260.357 i/s - 286.000 times in 1.098491s (3.84ms/i)
100KiB of 25B literals 19.485 i/s - 20.000 times in 1.026445s (51.32ms/i)
1KiB of 0B literals 506.675 i/s - 550.000 times in 1.085508s (1.97ms/i)
10KiB of 0B literals 44.384 i/s - 45.000 times in 1.013872s (22.53ms/i)
100KiB of 0B literals 3.063 i/s - 4.000 times in 1.305939s (326.48ms/i)
Calculating -------------------------------------
local YJIT
1KiB with no literals 194.355k 247.756k i/s - 608.261k times in 3.129645s 2.455086s
10KiB with no literals 55.733k 58.585k i/s - 167.049k times in 2.997311s 2.851414s
100KiB with no literals 6.553k 6.453k i/s - 19.961k times in 3.045870s 3.093461s
1KiB of 25B literals 2.732k 4.061k i/s - 8.340k times in 3.052737s 2.053682s
10KiB of 25B literals 256.552 379.524 i/s - 781.000 times in 3.044220s 2.057840s
100KiB of 25B literals 17.804 23.286 i/s - 58.000 times in 3.257733s 2.490779s
1KiB of 0B literals 467.714 703.446 i/s - 1.520k times in 3.249846s 2.160791s
10KiB of 0B literals 45.376 65.876 i/s - 133.000 times in 2.931045s 2.018955s
100KiB of 0B literals 3.072 3.840 i/s - 9.000 times in 2.929458s 2.343586s
Comparison:
1KiB with no literals
YJIT: 247755.5 i/s
local: 194354.7 i/s - 1.27x slower
10KiB with no literals
YJIT: 58584.6 i/s
local: 55733.0 i/s - 1.05x slower
100KiB with no literals
local: 6553.5 i/s
YJIT: 6452.6 i/s - 1.02x slower
1KiB of 25B literals
YJIT: 4061.0 i/s
local: 2732.0 i/s - 1.49x slower
10KiB of 25B literals
YJIT: 379.5 i/s
local: 256.6 i/s - 1.48x slower
100KiB of 25B literals
YJIT: 23.3 i/s
local: 17.8 i/s - 1.31x slower
1KiB of 0B literals
YJIT: 703.4 i/s
local: 467.7 i/s - 1.50x slower
10KiB of 0B literals
YJIT: 65.9 i/s
local: 45.4 i/s - 1.45x slower
100KiB of 0B literals
YJIT: 3.8 i/s
local: 3.1 i/s - 1.25x slower
```
---
lib/net/imap/response_reader.rb | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/lib/net/imap/response_reader.rb b/lib/net/imap/response_reader.rb
index d59d672..024d67a 100644
--- a/lib/net/imap/response_reader.rb
+++ b/lib/net/imap/response_reader.rb
@@ -32,7 +32,10 @@ module Net
def empty?; buff.empty? end
def done?; line_done? && !get_literal_size end
def line_done?; buff.end_with?(CRLF) end
- def get_literal_size; buff.rindex(/\{(\d+)\}\r\n\z/n) && $1.to_i end
+
+ def get_literal_size
+ buff.end_with?("}\r\n") && buff.rindex(/\{(\d+)\}\r\n\z/n) && $1.to_i
+ end
def read_line
buff << (@sock.gets(CRLF, read_limit) or throw :eof)
From 2e9d425aae61801deb18384fbd295cee6a9850a3 Mon Sep 17 00:00:00 2001
From: nick evans <nick@rubinick.dev>
Date: Tue, 14 Apr 2026 14:40:48 -0400
Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=8D=92=20pick=206f82e28f7:=20?=
=?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Faster=20ResponseReader:=20parse=20literal?=
=?UTF-8?q?=20from=20line?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Unfortunately, neither `#rindex` nor even `#end_with?` appear to be
truly `O(1)` for very large strings (100K+). I'm guessing that this is
due to memory locality and caching issues. But, by parsing the literal
from the latest `line` (rather than the full buffer), we mostly avoid
that problem.
Also, by explicitly parsing literal_size immediately after reading the
line, we don't need to parse it again in `#done?`.
```
$ benchmark-driver benchmarks/response_reader.yml --filter KiB
Warming up --------------------------------------
1KiB with no literals 202.846k i/s - 214.181k times in 1.055878s (4.93μs/i)
10KiB with no literals 55.699k i/s - 57.354k times in 1.029717s (17.95μs/i)
100KiB with no literals 6.622k i/s - 6.688k times in 1.009943s (151.01μs/i)
1KiB of 25B literals 3.428k i/s - 3.751k times in 1.094065s (291.67μs/i)
10KiB of 25B literals 342.733 i/s - 350.000 times in 1.021202s (2.92ms/i)
100KiB of 25B literals 34.343 i/s - 36.000 times in 1.048234s (29.12ms/i)
1KiB of 0B literals 683.800 i/s - 690.000 times in 1.009066s (1.46ms/i)
10KiB of 0B literals 69.186 i/s - 70.000 times in 1.011759s (14.45ms/i)
100KiB of 0B literals 6.914 i/s - 7.000 times in 1.012449s (144.64ms/i)
Calculating -------------------------------------
local YJIT
1KiB with no literals 193.622k 250.330k i/s - 608.539k times in 3.142929s 2.430944s
10KiB with no literals 55.944k 58.881k i/s - 167.096k times in 2.986849s 2.837843s
100KiB with no literals 6.550k 6.480k i/s - 19.866k times in 3.033041s 3.065821s
1KiB of 25B literals 3.445k 5.520k i/s - 10.285k times in 2.985693s 1.863057s
10KiB of 25B literals 338.578 548.670 i/s - 1.028k times in 3.036224s 1.873620s
100KiB of 25B literals 33.829 55.860 i/s - 103.000 times in 3.044728s 1.843900s
1KiB of 0B literals 626.970 1.103k i/s - 2.051k times in 3.271287s 1.860275s
10KiB of 0B literals 66.065 108.347 i/s - 207.000 times in 3.133301s 1.910523s
100KiB of 0B literals 6.720 8.159 i/s - 20.000 times in 2.976273s 2.451265s
Comparison:
1KiB with no literals
YJIT: 250330.3 i/s
local: 193621.6 i/s - 1.29x slower
10KiB with no literals
YJIT: 58881.3 i/s
local: 55943.9 i/s - 1.05x slower
100KiB with no literals
local: 6549.9 i/s
YJIT: 6479.8 i/s - 1.01x slower
1KiB of 25B literals
YJIT: 5520.5 i/s
local: 3444.8 i/s - 1.60x slower
10KiB of 25B literals
YJIT: 548.7 i/s
local: 338.6 i/s - 1.62x slower
100KiB of 25B literals
YJIT: 55.9 i/s
local: 33.8 i/s - 1.65x slower
1KiB of 0B literals
YJIT: 1102.5 i/s
local: 627.0 i/s - 1.76x slower
10KiB of 0B literals
YJIT: 108.3 i/s
local: 66.1 i/s - 1.64x slower
100KiB of 0B literals
YJIT: 8.2 i/s
local: 6.7 i/s - 1.21x slower
```
---
lib/net/imap/response_reader.rb | 13 ++++++++-----
1 file changed, 8 insertions(+), 5 deletions(-)
diff --git a/lib/net/imap/response_reader.rb b/lib/net/imap/response_reader.rb
index 024d67a..56958eb 100644
--- a/lib/net/imap/response_reader.rb
+++ b/lib/net/imap/response_reader.rb
@@ -8,6 +8,7 @@ module Net
def initialize(client, sock)
@client, @sock = client, sock
+ @buff = @literal_size = nil
end
def read_response_buffer
@@ -15,13 +16,13 @@ module Net
catch :eof do
while true
read_line
- break unless (@literal_size = get_literal_size)
+ break unless literal_size
read_literal
end
end
buff
ensure
- @buff = nil
+ @buff = @literal_size = nil
end
private
@@ -30,16 +31,18 @@ module Net
def bytes_read; buff.bytesize end
def empty?; buff.empty? end
- def done?; line_done? && !get_literal_size end
+ def done?; line_done? && !literal_size end
def line_done?; buff.end_with?(CRLF) end
- def get_literal_size
+ def get_literal_size(buff)
buff.end_with?("}\r\n") && buff.rindex(/\{(\d+)\}\r\n\z/n) && $1.to_i
end
def read_line
- buff << (@sock.gets(CRLF, read_limit) or throw :eof)
+ line = (@sock.gets(CRLF, read_limit) or throw :eof)
+ buff << line
max_response_remaining! unless line_done?
+ @literal_size = get_literal_size(line)
end
def read_literal

View File

@ -0,0 +1,116 @@
From 4244a308dc09682079ef461b68b08ad4ad9e5a57 Mon Sep 17 00:00:00 2001
From: nick evans <nick@rubinick.dev>
Date: Fri, 27 Mar 2026 17:31:11 -0400
Subject: [PATCH 1/2] =?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 84bc094..a8ac7d8 100644
--- a/lib/net/imap.rb
+++ b/lib/net/imap.rb
@@ -1294,9 +1294,11 @@ module Net
#
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
@@ -1308,6 +1310,13 @@ module Net
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 00192b9a410251c5ed76c96be564d739cba67008 Mon Sep 17 00:00:00 2001
From: nick evans <nick@rubinick.dev>
Date: Fri, 27 Mar 2026 18:00:09 -0400
Subject: [PATCH 2/2] =?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 a8ac7d8..b9072b1 100644
--- a/lib/net/imap.rb
+++ b/lib/net/imap.rb
@@ -3001,6 +3001,7 @@ module Net
put_string(" ")
send_data(i, tag)
end
+ guard_against_tagged_response_skipping_handler!(tag)
put_string(CRLF)
if cmd == "LOGOUT"
@logout_command_tag = tag
@@ -3016,6 +3017,17 @@ module Net
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