Skip to content

Commit 2f58d02

Browse files
committed
🔧 Add config option for max UIDPlusData size
A parser error will be raised when a `uid-set` contains more numbers than `config.parser_max_deprecated_uidplus_data_size`.
1 parent c674700 commit 2f58d02

File tree

3 files changed

+58
-3
lines changed

3 files changed

+58
-3
lines changed

lib/net/imap/config.rb

+34
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,12 @@ def self.[](config)
291291
# CopyUIDData for +COPYUID+ response codes, and UIDPlusData or
292292
# AppendUIDData for +APPENDUID+ response codes.
293293
#
294+
# UIDPlusData stores its data in arrays of numbers, which is vulnerable to
295+
# a memory exhaustion denial of service attack from an untrusted or
296+
# compromised server. Set this option to +false+ to completely block this
297+
# vulnerability. Otherwise, parser_max_deprecated_uidplus_data_size
298+
# mitigates this vulnerability.
299+
#
294300
# AppendUIDData and CopyUIDData are _mostly_ backward-compatible with
295301
# UIDPlusData. Most applications should be able to upgrade with little
296302
# or no changes.
@@ -313,6 +319,30 @@ def self.[](config)
313319
true, false
314320
]
315321

322+
# The maximum +uid-set+ size that ResponseParser will parse into
323+
# deprecated UIDPlusData. This limit only applies when
324+
# parser_use_deprecated_uidplus_data is not +false+.
325+
#
326+
# <em>(Parser support for +UIDPLUS+ added in +v0.3.2+.)</em>
327+
#
328+
# <em>Support for limiting UIDPlusData to a maximum size was added in
329+
# +v0.3.8+, +v0.4.19+, and +v0.5.6+.</em>
330+
#
331+
# <em>UIDPlusData will be removed in +v0.6+.</em>
332+
#
333+
# ==== Versioned Defaults
334+
#
335+
# Because this limit guards against a remote server causing catastrophic
336+
# memory exhaustion, the versioned default (used by #load_defaults) also
337+
# applies to versions without the feature.
338+
#
339+
# * +0.3+ and prior: <tt>10,000</tt>
340+
# * +0.4+: <tt>1,000</tt>
341+
# * +0.5+: <tt>100</tt>
342+
# * +0.6+: <tt>0</tt>
343+
#
344+
attr_accessor :parser_max_deprecated_uidplus_data_size, type: Integer
345+
316346
# Creates a new config object and initialize its attribute with +attrs+.
317347
#
318348
# If +parent+ is not given, the global config is used by default.
@@ -394,6 +424,7 @@ def defaults_hash
394424
enforce_logindisabled: true,
395425
responses_without_block: :warn,
396426
parser_use_deprecated_uidplus_data: true,
427+
parser_max_deprecated_uidplus_data_size: 100,
397428
).freeze
398429

399430
@global = default.new
@@ -406,6 +437,7 @@ def defaults_hash
406437
responses_without_block: :silence_deprecation_warning,
407438
enforce_logindisabled: false,
408439
parser_use_deprecated_uidplus_data: true,
440+
parser_max_deprecated_uidplus_data_size: 10_000,
409441
).freeze
410442
version_defaults[0.0] = Config[0]
411443
version_defaults[0.1] = Config[0]
@@ -414,13 +446,15 @@ def defaults_hash
414446

415447
version_defaults[0.4] = Config[0.3].dup.update(
416448
sasl_ir: true,
449+
parser_max_deprecated_uidplus_data_size: 1000,
417450
).freeze
418451

419452
version_defaults[0.5] = Config[:current]
420453

421454
version_defaults[0.6] = Config[0.5].dup.update(
422455
responses_without_block: :frozen_dup,
423456
parser_use_deprecated_uidplus_data: false,
457+
parser_max_deprecated_uidplus_data_size: 0,
424458
).freeze
425459
version_defaults[:next] = Config[0.6]
426460
version_defaults[:future] = Config[:next]

lib/net/imap/response_parser.rb

+1-3
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ class IMAP < Protocol
88

99
# Parses an \IMAP server response.
1010
class ResponseParser
11-
MAX_UID_SET_SIZE = 10_000
12-
1311
include ParserUtils
1412
extend ParserUtils::Generator
1513

@@ -2027,7 +2025,7 @@ def DeprecatedUIDPlus(validity, src_uids = nil, dst_uids)
20272025
return unless config.parser_use_deprecated_uidplus_data
20282026
compact_uid_sets = [src_uids, dst_uids].compact
20292027
count = compact_uid_sets.map { _1.count_with_duplicates }.max
2030-
max = MAX_UID_SET_SIZE
2028+
max = config.parser_max_deprecated_uidplus_data_size
20312029
if count <= max
20322030
src_uids &&= src_uids.each_ordered_number.to_a
20332031
dst_uids = dst_uids.each_ordered_number.to_a

test/net/imap/test_imap_response_parser.rb

+23
Original file line numberDiff line numberDiff line change
@@ -214,12 +214,22 @@ def test_fetch_binary_and_binary_size
214214
test "APPENDUID with parser_use_deprecated_uidplus_data = true" do
215215
parser = Net::IMAP::ResponseParser.new(config: {
216216
parser_use_deprecated_uidplus_data: true,
217+
parser_max_deprecated_uidplus_data_size: 10_000,
217218
})
218219
assert_raise_with_message Net::IMAP::ResponseParseError, /uid-set is too large/ do
219220
parser.parse(
220221
"A004 OK [APPENDUID 1 10000:20000,1] Done\r\n"
221222
)
222223
end
224+
response = parser.parse("A004 OK [APPENDUID 1 100:200] Done\r\n")
225+
uidplus = response.data.code.data
226+
assert_equal 101, uidplus.assigned_uids.size
227+
parser.config.parser_max_deprecated_uidplus_data_size = 100
228+
assert_raise_with_message Net::IMAP::ResponseParseError, /uid-set is too large/ do
229+
parser.parse(
230+
"A004 OK [APPENDUID 1 100:200] Done\r\n"
231+
)
232+
end
223233
response = parser.parse("A004 OK [APPENDUID 1 101:200] Done\r\n")
224234
uidplus = response.data.code.data
225235
assert_instance_of Net::IMAP::UIDPlusData, uidplus
@@ -229,6 +239,7 @@ def test_fetch_binary_and_binary_size
229239
test "APPENDUID with parser_use_deprecated_uidplus_data = false" do
230240
parser = Net::IMAP::ResponseParser.new(config: {
231241
parser_use_deprecated_uidplus_data: false,
242+
parser_max_deprecated_uidplus_data_size: 10_000_000,
232243
})
233244
response = parser.parse("A004 OK [APPENDUID 1 10] Done\r\n")
234245
assert_instance_of Net::IMAP::AppendUIDData, response.data.code.data
@@ -267,12 +278,23 @@ def test_fetch_binary_and_binary_size
267278
test "COPYUID with parser_use_deprecated_uidplus_data = true" do
268279
parser = Net::IMAP::ResponseParser.new(config: {
269280
parser_use_deprecated_uidplus_data: true,
281+
parser_max_deprecated_uidplus_data_size: 10_000,
270282
})
271283
assert_raise_with_message Net::IMAP::ResponseParseError, /uid-set is too large/ do
272284
parser.parse(
273285
"A004 OK [copyUID 1 10000:20000,1 1:10001] Done\r\n"
274286
)
275287
end
288+
response = parser.parse("A004 OK [copyUID 1 100:200 1:101] Done\r\n")
289+
uidplus = response.data.code.data
290+
assert_equal 101, uidplus.assigned_uids.size
291+
assert_equal 101, uidplus.source_uids.size
292+
parser.config.parser_max_deprecated_uidplus_data_size = 100
293+
assert_raise_with_message Net::IMAP::ResponseParseError, /uid-set is too large/ do
294+
parser.parse(
295+
"A004 OK [copyUID 1 100:200 1:101] Done\r\n"
296+
)
297+
end
276298
response = parser.parse("A004 OK [copyUID 1 101:200 1:100] Done\r\n")
277299
uidplus = response.data.code.data
278300
assert_instance_of Net::IMAP::UIDPlusData, uidplus
@@ -283,6 +305,7 @@ def test_fetch_binary_and_binary_size
283305
test "COPYUID with parser_use_deprecated_uidplus_data = false" do
284306
parser = Net::IMAP::ResponseParser.new(config: {
285307
parser_use_deprecated_uidplus_data: false,
308+
parser_max_deprecated_uidplus_data_size: 10_000_000,
286309
})
287310
response = parser.parse("A004 OK [COPYUID 1 101 1] Done\r\n")
288311
assert_instance_of Net::IMAP::CopyUIDData, response.data.code.data

0 commit comments

Comments
 (0)