Skip to content

Commit

Permalink
🔧 Add config option for max UIDPlusData size
Browse files Browse the repository at this point in the history
A parser error will be raised when a `uid-set` contains more numbers
than `config.parser_max_deprecated_uidplus_data_size`.
  • Loading branch information
nevans committed Feb 7, 2025
1 parent 6613d57 commit 34a1f27
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 3 deletions.
34 changes: 34 additions & 0 deletions lib/net/imap/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,12 @@ def self.[](config)
# CopyUIDData for +COPYUID+ response codes, and UIDPlusData or
# AppendUIDData for +APPENDUID+ response codes.
#
# UIDPlusData stores its data in arrays of numbers, which is vulnerable to
# a memory exhaustion denial of service attack from an untrusted or
# compromised server. Set this option to +false+ to completely block this
# vulnerability. Otherwise, parser_max_deprecated_uidplus_data_size
# mitigates this vulnerability.
#
# AppendUIDData and CopyUIDData are _mostly_ backward-compatible with
# UIDPlusData. Most applications should be able to upgrade with little
# or no changes.
Expand All @@ -288,6 +294,30 @@ def self.[](config)
true, false
]

# The maximum +uid-set+ size that ResponseParser will parse into
# deprecated UIDPlusData. This limit only applies when
# parser_use_deprecated_uidplus_data is not +false+.
#
# <em>(Parser support for +UIDPLUS+ added in +v0.3.2+.)</em>
#
# <em>Support for limiting UIDPlusData to a maximum size was added in
# +v0.3.8+, +v0.4.19+, and +v0.5.6+.</em>
#
# <em>UIDPlusData will be removed in +v0.6+.</em>
#
# ==== Versioned Defaults
#
# Because this limit guards against a remote server causing catastrophic
# memory exhaustion, the versioned default (used by #load_defaults) also
# applies to versions without the feature.
#
# * +0.3+ and prior: <tt>10,000</tt>
# * +0.4+: <tt>1,000</tt>
# * +0.5+: <tt>100</tt>
# * +0.6+: <tt>0</tt>
#
attr_accessor :parser_max_deprecated_uidplus_data_size, type: Integer

# Creates a new config object and initialize its attribute with +attrs+.
#
# If +parent+ is not given, the global config is used by default.
Expand Down Expand Up @@ -368,6 +398,7 @@ def defaults_hash
sasl_ir: true,
responses_without_block: :silence_deprecation_warning,
parser_use_deprecated_uidplus_data: true,
parser_max_deprecated_uidplus_data_size: 1000,
).freeze

@global = default.new
Expand All @@ -377,6 +408,7 @@ def defaults_hash
version_defaults[0] = Config[0.4].dup.update(
sasl_ir: false,
parser_use_deprecated_uidplus_data: true,
parser_max_deprecated_uidplus_data_size: 10_000,
).freeze
version_defaults[0.0] = Config[0]
version_defaults[0.1] = Config[0]
Expand All @@ -385,6 +417,7 @@ def defaults_hash

version_defaults[0.5] = Config[0.4].dup.update(
responses_without_block: :warn,
parser_max_deprecated_uidplus_data_size: 100,
).freeze

version_defaults[:default] = Config[0.4]
Expand All @@ -394,6 +427,7 @@ def defaults_hash
version_defaults[0.6] = Config[0.5].dup.update(
responses_without_block: :frozen_dup,
parser_use_deprecated_uidplus_data: false,
parser_max_deprecated_uidplus_data_size: 0,
).freeze
version_defaults[:future] = Config[0.6]

Expand Down
4 changes: 1 addition & 3 deletions lib/net/imap/response_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ class IMAP < Protocol

# Parses an \IMAP server response.
class ResponseParser
MAX_UID_SET_SIZE = 10_000

include ParserUtils
extend ParserUtils::Generator

Expand Down Expand Up @@ -1893,7 +1891,7 @@ def DeprecatedUIDPlus(validity, src_uids = nil, dst_uids)
return unless config.parser_use_deprecated_uidplus_data
compact_uid_sets = [src_uids, dst_uids].compact
count = compact_uid_sets.map { _1.count_with_duplicates }.max
max = MAX_UID_SET_SIZE
max = config.parser_max_deprecated_uidplus_data_size
if count <= max
src_uids &&= src_uids.each_ordered_number.to_a
dst_uids = dst_uids.each_ordered_number.to_a
Expand Down
23 changes: 23 additions & 0 deletions test/net/imap/test_imap_response_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -205,12 +205,22 @@ def test_fetch_binary_and_binary_size
test "APPENDUID with parser_use_deprecated_uidplus_data = true" do
parser = Net::IMAP::ResponseParser.new(config: {
parser_use_deprecated_uidplus_data: true,
parser_max_deprecated_uidplus_data_size: 10_000,
})
assert_raise_with_message Net::IMAP::ResponseParseError, /uid-set is too large/ do
parser.parse(
"A004 OK [APPENDUID 1 10000:20000,1] Done\r\n"
)
end
response = parser.parse("A004 OK [APPENDUID 1 100:200] Done\r\n")
uidplus = response.data.code.data
assert_equal 101, uidplus.assigned_uids.size
parser.config.parser_max_deprecated_uidplus_data_size = 100
assert_raise_with_message Net::IMAP::ResponseParseError, /uid-set is too large/ do
parser.parse(
"A004 OK [APPENDUID 1 100:200] Done\r\n"
)
end
response = parser.parse("A004 OK [APPENDUID 1 101:200] Done\r\n")
uidplus = response.data.code.data
assert_instance_of Net::IMAP::UIDPlusData, uidplus
Expand All @@ -220,6 +230,7 @@ def test_fetch_binary_and_binary_size
test "APPENDUID with parser_use_deprecated_uidplus_data = false" do
parser = Net::IMAP::ResponseParser.new(config: {
parser_use_deprecated_uidplus_data: false,
parser_max_deprecated_uidplus_data_size: 10_000_000,
})
response = parser.parse("A004 OK [APPENDUID 1 10] Done\r\n")
assert_instance_of Net::IMAP::AppendUIDData, response.data.code.data
Expand Down Expand Up @@ -258,12 +269,23 @@ def test_fetch_binary_and_binary_size
test "COPYUID with parser_use_deprecated_uidplus_data = true" do
parser = Net::IMAP::ResponseParser.new(config: {
parser_use_deprecated_uidplus_data: true,
parser_max_deprecated_uidplus_data_size: 10_000,
})
assert_raise_with_message Net::IMAP::ResponseParseError, /uid-set is too large/ do
parser.parse(
"A004 OK [copyUID 1 10000:20000,1 1:10001] Done\r\n"
)
end
response = parser.parse("A004 OK [copyUID 1 100:200 1:101] Done\r\n")
uidplus = response.data.code.data
assert_equal 101, uidplus.assigned_uids.size
assert_equal 101, uidplus.source_uids.size
parser.config.parser_max_deprecated_uidplus_data_size = 100
assert_raise_with_message Net::IMAP::ResponseParseError, /uid-set is too large/ do
parser.parse(
"A004 OK [copyUID 1 100:200 1:101] Done\r\n"
)
end
response = parser.parse("A004 OK [copyUID 1 101:200 1:100] Done\r\n")
uidplus = response.data.code.data
assert_instance_of Net::IMAP::UIDPlusData, uidplus
Expand All @@ -274,6 +296,7 @@ def test_fetch_binary_and_binary_size
test "COPYUID with parser_use_deprecated_uidplus_data = false" do
parser = Net::IMAP::ResponseParser.new(config: {
parser_use_deprecated_uidplus_data: false,
parser_max_deprecated_uidplus_data_size: 10_000_000,
})
response = parser.parse("A004 OK [COPYUID 1 101 1] Done\r\n")
assert_instance_of Net::IMAP::CopyUIDData, response.data.code.data
Expand Down

0 comments on commit 34a1f27

Please sign in to comment.