Skip to content

Commit 70e3ddd

Browse files
authored
Merge commit from fork
🔒 Prevent runaway memory use when parsing uid-set
2 parents 60f5776 + e58aff6 commit 70e3ddd

File tree

3 files changed

+108
-5
lines changed

3 files changed

+108
-5
lines changed

lib/net/imap/config.rb

+41-2
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.
@@ -307,12 +313,41 @@ def self.[](config)
307313
# [+true+ <em>(original default)</em>]
308314
# ResponseParser only uses UIDPlusData.
309315
#
316+
# [+:up_to_max_size+ <em>(default since +v0.5.6+)</em>]
317+
# ResponseParser uses UIDPlusData when the +uid-set+ size is below
318+
# parser_max_deprecated_uidplus_data_size. Above that size,
319+
# ResponseParser uses AppendUIDData or CopyUIDData.
320+
#
310321
# [+false+ <em>(planned default for +v0.6+)</em>]
311322
# ResponseParser _only_ uses AppendUIDData and CopyUIDData.
312323
attr_accessor :parser_use_deprecated_uidplus_data, type: [
313-
true, false
324+
true, :up_to_max_size, false
314325
]
315326

327+
# The maximum +uid-set+ size that ResponseParser will parse into
328+
# deprecated UIDPlusData. This limit only applies when
329+
# parser_use_deprecated_uidplus_data is not +false+.
330+
#
331+
# <em>(Parser support for +UIDPLUS+ added in +v0.3.2+.)</em>
332+
#
333+
# <em>Support for limiting UIDPlusData to a maximum size was added in
334+
# +v0.3.8+, +v0.4.19+, and +v0.5.6+.</em>
335+
#
336+
# <em>UIDPlusData will be removed in +v0.6+.</em>
337+
#
338+
# ==== Versioned Defaults
339+
#
340+
# Because this limit guards against a remote server causing catastrophic
341+
# memory exhaustion, the versioned default (used by #load_defaults) also
342+
# applies to versions without the feature.
343+
#
344+
# * +0.3+ and prior: <tt>10,000</tt>
345+
# * +0.4+: <tt>1,000</tt>
346+
# * +0.5+: <tt>100</tt>
347+
# * +0.6+: <tt>0</tt>
348+
#
349+
attr_accessor :parser_max_deprecated_uidplus_data_size, type: Integer
350+
316351
# Creates a new config object and initialize its attribute with +attrs+.
317352
#
318353
# If +parent+ is not given, the global config is used by default.
@@ -393,7 +428,8 @@ def defaults_hash
393428
sasl_ir: true,
394429
enforce_logindisabled: true,
395430
responses_without_block: :warn,
396-
parser_use_deprecated_uidplus_data: true,
431+
parser_use_deprecated_uidplus_data: :up_to_max_size,
432+
parser_max_deprecated_uidplus_data_size: 100,
397433
).freeze
398434

399435
@global = default.new
@@ -406,6 +442,7 @@ def defaults_hash
406442
responses_without_block: :silence_deprecation_warning,
407443
enforce_logindisabled: false,
408444
parser_use_deprecated_uidplus_data: true,
445+
parser_max_deprecated_uidplus_data_size: 10_000,
409446
).freeze
410447
version_defaults[0.0] = Config[0]
411448
version_defaults[0.1] = Config[0]
@@ -414,13 +451,15 @@ def defaults_hash
414451

415452
version_defaults[0.4] = Config[0.3].dup.update(
416453
sasl_ir: true,
454+
parser_max_deprecated_uidplus_data_size: 1000,
417455
).freeze
418456

419457
version_defaults[0.5] = Config[:current]
420458

421459
version_defaults[0.6] = Config[0.5].dup.update(
422460
responses_without_block: :frozen_dup,
423461
parser_use_deprecated_uidplus_data: false,
462+
parser_max_deprecated_uidplus_data_size: 0,
424463
).freeze
425464
version_defaults[:next] = Config[0.6]
426465
version_defaults[:future] = Config[:next]

lib/net/imap/response_parser.rb

+10-3
Original file line numberDiff line numberDiff line change
@@ -2023,9 +2023,16 @@ def CopyUID(...) DeprecatedUIDPlus(...) || CopyUIDData.new(...) end
20232023
# TODO: remove this code in the v0.6.0 release
20242024
def DeprecatedUIDPlus(validity, src_uids = nil, dst_uids)
20252025
return unless config.parser_use_deprecated_uidplus_data
2026-
src_uids &&= src_uids.each_ordered_number.to_a
2027-
dst_uids = dst_uids.each_ordered_number.to_a
2028-
UIDPlusData.new(validity, src_uids, dst_uids)
2026+
compact_uid_sets = [src_uids, dst_uids].compact
2027+
count = compact_uid_sets.map { _1.count_with_duplicates }.max
2028+
max = config.parser_max_deprecated_uidplus_data_size
2029+
if count <= max
2030+
src_uids &&= src_uids.each_ordered_number.to_a
2031+
dst_uids = dst_uids.each_ordered_number.to_a
2032+
UIDPlusData.new(validity, src_uids, dst_uids)
2033+
elsif config.parser_use_deprecated_uidplus_data != :up_to_max_size
2034+
parse_error("uid-set is too large: %d > %d", count, max)
2035+
end
20292036
end
20302037

20312038
ADDRESS_REGEXP = /\G

test/net/imap/test_imap_response_parser.rb

+57
Original file line numberDiff line numberDiff line change
@@ -214,16 +214,43 @@ 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
})
219+
assert_raise_with_message Net::IMAP::ResponseParseError, /uid-set is too large/ do
220+
parser.parse(
221+
"A004 OK [APPENDUID 1 10000:20000,1] Done\r\n"
222+
)
223+
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
218233
response = parser.parse("A004 OK [APPENDUID 1 101:200] Done\r\n")
219234
uidplus = response.data.code.data
220235
assert_instance_of Net::IMAP::UIDPlusData, uidplus
221236
assert_equal 100, uidplus.assigned_uids.size
222237
end
223238

239+
test "APPENDUID with parser_use_deprecated_uidplus_data = :up_to_max_size" do
240+
parser = Net::IMAP::ResponseParser.new(config: {
241+
parser_use_deprecated_uidplus_data: :up_to_max_size,
242+
parser_max_deprecated_uidplus_data_size: 100
243+
})
244+
response = parser.parse("A004 OK [APPENDUID 1 101:200] Done\r\n")
245+
assert_instance_of Net::IMAP::UIDPlusData, response.data.code.data
246+
response = parser.parse("A004 OK [APPENDUID 1 100:200] Done\r\n")
247+
assert_instance_of Net::IMAP::AppendUIDData, response.data.code.data
248+
end
249+
224250
test "APPENDUID with parser_use_deprecated_uidplus_data = false" do
225251
parser = Net::IMAP::ResponseParser.new(config: {
226252
parser_use_deprecated_uidplus_data: false,
253+
parser_max_deprecated_uidplus_data_size: 10_000_000,
227254
})
228255
response = parser.parse("A004 OK [APPENDUID 1 10] Done\r\n")
229256
assert_instance_of Net::IMAP::AppendUIDData, response.data.code.data
@@ -262,17 +289,47 @@ def test_fetch_binary_and_binary_size
262289
test "COPYUID with parser_use_deprecated_uidplus_data = true" do
263290
parser = Net::IMAP::ResponseParser.new(config: {
264291
parser_use_deprecated_uidplus_data: true,
292+
parser_max_deprecated_uidplus_data_size: 10_000,
265293
})
294+
assert_raise_with_message Net::IMAP::ResponseParseError, /uid-set is too large/ do
295+
parser.parse(
296+
"A004 OK [copyUID 1 10000:20000,1 1:10001] Done\r\n"
297+
)
298+
end
299+
response = parser.parse("A004 OK [copyUID 1 100:200 1:101] Done\r\n")
300+
uidplus = response.data.code.data
301+
assert_equal 101, uidplus.assigned_uids.size
302+
assert_equal 101, uidplus.source_uids.size
303+
parser.config.parser_max_deprecated_uidplus_data_size = 100
304+
assert_raise_with_message Net::IMAP::ResponseParseError, /uid-set is too large/ do
305+
parser.parse(
306+
"A004 OK [copyUID 1 100:200 1:101] Done\r\n"
307+
)
308+
end
266309
response = parser.parse("A004 OK [copyUID 1 101:200 1:100] Done\r\n")
267310
uidplus = response.data.code.data
268311
assert_instance_of Net::IMAP::UIDPlusData, uidplus
269312
assert_equal 100, uidplus.assigned_uids.size
270313
assert_equal 100, uidplus.source_uids.size
271314
end
272315

316+
test "COPYUID with parser_use_deprecated_uidplus_data = :up_to_max_size" do
317+
parser = Net::IMAP::ResponseParser.new(config: {
318+
parser_use_deprecated_uidplus_data: :up_to_max_size,
319+
parser_max_deprecated_uidplus_data_size: 100
320+
})
321+
response = parser.parse("A004 OK [COPYUID 1 101:200 1:100] Done\r\n")
322+
copyuid = response.data.code.data
323+
assert_instance_of Net::IMAP::UIDPlusData, copyuid
324+
response = parser.parse("A004 OK [COPYUID 1 100:200 1:101] Done\r\n")
325+
copyuid = response.data.code.data
326+
assert_instance_of Net::IMAP::CopyUIDData, copyuid
327+
end
328+
273329
test "COPYUID with parser_use_deprecated_uidplus_data = false" do
274330
parser = Net::IMAP::ResponseParser.new(config: {
275331
parser_use_deprecated_uidplus_data: false,
332+
parser_max_deprecated_uidplus_data_size: 10_000_000,
276333
})
277334
response = parser.parse("A004 OK [COPYUID 1 101 1] Done\r\n")
278335
assert_instance_of Net::IMAP::CopyUIDData, response.data.code.data

0 commit comments

Comments
 (0)