Skip to content

Commit c8c5a64

Browse files
authored
Merge commit from fork
🔒 Prevent runaway memory use when parsing `uid-set` (v0.4 backport)
2 parents e4d57b1 + abff00f commit c8c5a64

File tree

3 files changed

+108
-4
lines changed

3 files changed

+108
-4
lines changed

lib/net/imap/config.rb

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,12 @@ def self.[](config)
266266
# CopyUIDData for +COPYUID+ response codes, and UIDPlusData or
267267
# AppendUIDData for +APPENDUID+ response codes.
268268
#
269+
# UIDPlusData stores its data in arrays of numbers, which is vulnerable to
270+
# a memory exhaustion denial of service attack from an untrusted or
271+
# compromised server. Set this option to +false+ to completely block this
272+
# vulnerability. Otherwise, parser_max_deprecated_uidplus_data_size
273+
# mitigates this vulnerability.
274+
#
269275
# AppendUIDData and CopyUIDData are _mostly_ backward-compatible with
270276
# UIDPlusData. Most applications should be able to upgrade with little
271277
# or no changes.
@@ -282,12 +288,41 @@ def self.[](config)
282288
# [+true+ <em>(original default)</em>]
283289
# ResponseParser only uses UIDPlusData.
284290
#
291+
# [+:up_to_max_size+ <em>(default since +v0.5.6+)</em>]
292+
# ResponseParser uses UIDPlusData when the +uid-set+ size is below
293+
# parser_max_deprecated_uidplus_data_size. Above that size,
294+
# ResponseParser uses AppendUIDData or CopyUIDData.
295+
#
285296
# [+false+ <em>(planned default for +v0.6+)</em>]
286297
# ResponseParser _only_ uses AppendUIDData and CopyUIDData.
287298
attr_accessor :parser_use_deprecated_uidplus_data, type: [
288-
true, false
299+
true, :up_to_max_size, false
289300
]
290301

302+
# The maximum +uid-set+ size that ResponseParser will parse into
303+
# deprecated UIDPlusData. This limit only applies when
304+
# parser_use_deprecated_uidplus_data is not +false+.
305+
#
306+
# <em>(Parser support for +UIDPLUS+ added in +v0.3.2+.)</em>
307+
#
308+
# <em>Support for limiting UIDPlusData to a maximum size was added in
309+
# +v0.3.8+, +v0.4.19+, and +v0.5.6+.</em>
310+
#
311+
# <em>UIDPlusData will be removed in +v0.6+.</em>
312+
#
313+
# ==== Versioned Defaults
314+
#
315+
# Because this limit guards against a remote server causing catastrophic
316+
# memory exhaustion, the versioned default (used by #load_defaults) also
317+
# applies to versions without the feature.
318+
#
319+
# * +0.3+ and prior: <tt>10,000</tt>
320+
# * +0.4+: <tt>1,000</tt>
321+
# * +0.5+: <tt>100</tt>
322+
# * +0.6+: <tt>0</tt>
323+
#
324+
attr_accessor :parser_max_deprecated_uidplus_data_size, type: Integer
325+
291326
# Creates a new config object and initialize its attribute with +attrs+.
292327
#
293328
# If +parent+ is not given, the global config is used by default.
@@ -368,6 +403,7 @@ def defaults_hash
368403
sasl_ir: true,
369404
responses_without_block: :silence_deprecation_warning,
370405
parser_use_deprecated_uidplus_data: true,
406+
parser_max_deprecated_uidplus_data_size: 1000,
371407
).freeze
372408

373409
@global = default.new
@@ -377,6 +413,7 @@ def defaults_hash
377413
version_defaults[0] = Config[0.4].dup.update(
378414
sasl_ir: false,
379415
parser_use_deprecated_uidplus_data: true,
416+
parser_max_deprecated_uidplus_data_size: 10_000,
380417
).freeze
381418
version_defaults[0.0] = Config[0]
382419
version_defaults[0.1] = Config[0]
@@ -385,6 +422,8 @@ def defaults_hash
385422

386423
version_defaults[0.5] = Config[0.4].dup.update(
387424
responses_without_block: :warn,
425+
parser_use_deprecated_uidplus_data: :up_to_max_size,
426+
parser_max_deprecated_uidplus_data_size: 100,
388427
).freeze
389428

390429
version_defaults[:default] = Config[0.4]
@@ -394,6 +433,7 @@ def defaults_hash
394433
version_defaults[0.6] = Config[0.5].dup.update(
395434
responses_without_block: :frozen_dup,
396435
parser_use_deprecated_uidplus_data: false,
436+
parser_max_deprecated_uidplus_data_size: 0,
397437
).freeze
398438
version_defaults[:future] = Config[0.6]
399439

lib/net/imap/response_parser.rb

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1889,9 +1889,16 @@ def CopyUID(...) DeprecatedUIDPlus(...) || CopyUIDData.new(...) end
18891889
# TODO: remove this code in the v0.6.0 release
18901890
def DeprecatedUIDPlus(validity, src_uids = nil, dst_uids)
18911891
return unless config.parser_use_deprecated_uidplus_data
1892-
src_uids &&= src_uids.each_ordered_number.to_a
1893-
dst_uids = dst_uids.each_ordered_number.to_a
1894-
UIDPlusData.new(validity, src_uids, dst_uids)
1892+
compact_uid_sets = [src_uids, dst_uids].compact
1893+
count = compact_uid_sets.map { _1.count_with_duplicates }.max
1894+
max = config.parser_max_deprecated_uidplus_data_size
1895+
if count <= max
1896+
src_uids &&= src_uids.each_ordered_number.to_a
1897+
dst_uids = dst_uids.each_ordered_number.to_a
1898+
UIDPlusData.new(validity, src_uids, dst_uids)
1899+
elsif config.parser_use_deprecated_uidplus_data != :up_to_max_size
1900+
parse_error("uid-set is too large: %d > %d", count, max)
1901+
end
18951902
end
18961903

18971904
ADDRESS_REGEXP = /\G

test/net/imap/test_imap_response_parser.rb

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,16 +205,43 @@ def test_fetch_binary_and_binary_size
205205
test "APPENDUID with parser_use_deprecated_uidplus_data = true" do
206206
parser = Net::IMAP::ResponseParser.new(config: {
207207
parser_use_deprecated_uidplus_data: true,
208+
parser_max_deprecated_uidplus_data_size: 10_000,
208209
})
210+
assert_raise_with_message Net::IMAP::ResponseParseError, /uid-set is too large/ do
211+
parser.parse(
212+
"A004 OK [APPENDUID 1 10000:20000,1] Done\r\n"
213+
)
214+
end
215+
response = parser.parse("A004 OK [APPENDUID 1 100:200] Done\r\n")
216+
uidplus = response.data.code.data
217+
assert_equal 101, uidplus.assigned_uids.size
218+
parser.config.parser_max_deprecated_uidplus_data_size = 100
219+
assert_raise_with_message Net::IMAP::ResponseParseError, /uid-set is too large/ do
220+
parser.parse(
221+
"A004 OK [APPENDUID 1 100:200] Done\r\n"
222+
)
223+
end
209224
response = parser.parse("A004 OK [APPENDUID 1 101:200] Done\r\n")
210225
uidplus = response.data.code.data
211226
assert_instance_of Net::IMAP::UIDPlusData, uidplus
212227
assert_equal 100, uidplus.assigned_uids.size
213228
end
214229

230+
test "APPENDUID with parser_use_deprecated_uidplus_data = :up_to_max_size" do
231+
parser = Net::IMAP::ResponseParser.new(config: {
232+
parser_use_deprecated_uidplus_data: :up_to_max_size,
233+
parser_max_deprecated_uidplus_data_size: 100
234+
})
235+
response = parser.parse("A004 OK [APPENDUID 1 101:200] Done\r\n")
236+
assert_instance_of Net::IMAP::UIDPlusData, response.data.code.data
237+
response = parser.parse("A004 OK [APPENDUID 1 100:200] Done\r\n")
238+
assert_instance_of Net::IMAP::AppendUIDData, response.data.code.data
239+
end
240+
215241
test "APPENDUID with parser_use_deprecated_uidplus_data = false" do
216242
parser = Net::IMAP::ResponseParser.new(config: {
217243
parser_use_deprecated_uidplus_data: false,
244+
parser_max_deprecated_uidplus_data_size: 10_000_000,
218245
})
219246
response = parser.parse("A004 OK [APPENDUID 1 10] Done\r\n")
220247
assert_instance_of Net::IMAP::AppendUIDData, response.data.code.data
@@ -253,17 +280,47 @@ def test_fetch_binary_and_binary_size
253280
test "COPYUID with parser_use_deprecated_uidplus_data = true" do
254281
parser = Net::IMAP::ResponseParser.new(config: {
255282
parser_use_deprecated_uidplus_data: true,
283+
parser_max_deprecated_uidplus_data_size: 10_000,
256284
})
285+
assert_raise_with_message Net::IMAP::ResponseParseError, /uid-set is too large/ do
286+
parser.parse(
287+
"A004 OK [copyUID 1 10000:20000,1 1:10001] Done\r\n"
288+
)
289+
end
290+
response = parser.parse("A004 OK [copyUID 1 100:200 1:101] Done\r\n")
291+
uidplus = response.data.code.data
292+
assert_equal 101, uidplus.assigned_uids.size
293+
assert_equal 101, uidplus.source_uids.size
294+
parser.config.parser_max_deprecated_uidplus_data_size = 100
295+
assert_raise_with_message Net::IMAP::ResponseParseError, /uid-set is too large/ do
296+
parser.parse(
297+
"A004 OK [copyUID 1 100:200 1:101] Done\r\n"
298+
)
299+
end
257300
response = parser.parse("A004 OK [copyUID 1 101:200 1:100] Done\r\n")
258301
uidplus = response.data.code.data
259302
assert_instance_of Net::IMAP::UIDPlusData, uidplus
260303
assert_equal 100, uidplus.assigned_uids.size
261304
assert_equal 100, uidplus.source_uids.size
262305
end
263306

307+
test "COPYUID with parser_use_deprecated_uidplus_data = :up_to_max_size" do
308+
parser = Net::IMAP::ResponseParser.new(config: {
309+
parser_use_deprecated_uidplus_data: :up_to_max_size,
310+
parser_max_deprecated_uidplus_data_size: 100
311+
})
312+
response = parser.parse("A004 OK [COPYUID 1 101:200 1:100] Done\r\n")
313+
copyuid = response.data.code.data
314+
assert_instance_of Net::IMAP::UIDPlusData, copyuid
315+
response = parser.parse("A004 OK [COPYUID 1 100:200 1:101] Done\r\n")
316+
copyuid = response.data.code.data
317+
assert_instance_of Net::IMAP::CopyUIDData, copyuid
318+
end
319+
264320
test "COPYUID with parser_use_deprecated_uidplus_data = false" do
265321
parser = Net::IMAP::ResponseParser.new(config: {
266322
parser_use_deprecated_uidplus_data: false,
323+
parser_max_deprecated_uidplus_data_size: 10_000_000,
267324
})
268325
response = parser.parse("A004 OK [COPYUID 1 101 1] Done\r\n")
269326
assert_instance_of Net::IMAP::CopyUIDData, response.data.code.data

0 commit comments

Comments
 (0)