Skip to content

Commit 34a1f27

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 6613d57 commit 34a1f27

File tree

3 files changed

+58
-3
lines changed

3 files changed

+58
-3
lines changed

lib/net/imap/config.rb

Lines changed: 34 additions & 0 deletions
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.
@@ -288,6 +294,30 @@ def self.[](config)
288294
true, false
289295
]
290296

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

373404
@global = default.new
@@ -377,6 +408,7 @@ def defaults_hash
377408
version_defaults[0] = Config[0.4].dup.update(
378409
sasl_ir: false,
379410
parser_use_deprecated_uidplus_data: true,
411+
parser_max_deprecated_uidplus_data_size: 10_000,
380412
).freeze
381413
version_defaults[0.0] = Config[0]
382414
version_defaults[0.1] = Config[0]
@@ -385,6 +417,7 @@ def defaults_hash
385417

386418
version_defaults[0.5] = Config[0.4].dup.update(
387419
responses_without_block: :warn,
420+
parser_max_deprecated_uidplus_data_size: 100,
388421
).freeze
389422

390423
version_defaults[:default] = Config[0.4]
@@ -394,6 +427,7 @@ def defaults_hash
394427
version_defaults[0.6] = Config[0.5].dup.update(
395428
responses_without_block: :frozen_dup,
396429
parser_use_deprecated_uidplus_data: false,
430+
parser_max_deprecated_uidplus_data_size: 0,
397431
).freeze
398432
version_defaults[:future] = Config[0.6]
399433

lib/net/imap/response_parser.rb

Lines changed: 1 addition & 3 deletions
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

@@ -1893,7 +1891,7 @@ def DeprecatedUIDPlus(validity, src_uids = nil, dst_uids)
18931891
return unless config.parser_use_deprecated_uidplus_data
18941892
compact_uid_sets = [src_uids, dst_uids].compact
18951893
count = compact_uid_sets.map { _1.count_with_duplicates }.max
1896-
max = MAX_UID_SET_SIZE
1894+
max = config.parser_max_deprecated_uidplus_data_size
18971895
if count <= max
18981896
src_uids &&= src_uids.each_ordered_number.to_a
18991897
dst_uids = dst_uids.each_ordered_number.to_a

test/net/imap/test_imap_response_parser.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,12 +205,22 @@ 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
})
209210
assert_raise_with_message Net::IMAP::ResponseParseError, /uid-set is too large/ do
210211
parser.parse(
211212
"A004 OK [APPENDUID 1 10000:20000,1] Done\r\n"
212213
)
213214
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
214224
response = parser.parse("A004 OK [APPENDUID 1 101:200] Done\r\n")
215225
uidplus = response.data.code.data
216226
assert_instance_of Net::IMAP::UIDPlusData, uidplus
@@ -220,6 +230,7 @@ def test_fetch_binary_and_binary_size
220230
test "APPENDUID with parser_use_deprecated_uidplus_data = false" do
221231
parser = Net::IMAP::ResponseParser.new(config: {
222232
parser_use_deprecated_uidplus_data: false,
233+
parser_max_deprecated_uidplus_data_size: 10_000_000,
223234
})
224235
response = parser.parse("A004 OK [APPENDUID 1 10] Done\r\n")
225236
assert_instance_of Net::IMAP::AppendUIDData, response.data.code.data
@@ -258,12 +269,23 @@ def test_fetch_binary_and_binary_size
258269
test "COPYUID with parser_use_deprecated_uidplus_data = true" do
259270
parser = Net::IMAP::ResponseParser.new(config: {
260271
parser_use_deprecated_uidplus_data: true,
272+
parser_max_deprecated_uidplus_data_size: 10_000,
261273
})
262274
assert_raise_with_message Net::IMAP::ResponseParseError, /uid-set is too large/ do
263275
parser.parse(
264276
"A004 OK [copyUID 1 10000:20000,1 1:10001] Done\r\n"
265277
)
266278
end
279+
response = parser.parse("A004 OK [copyUID 1 100:200 1:101] Done\r\n")
280+
uidplus = response.data.code.data
281+
assert_equal 101, uidplus.assigned_uids.size
282+
assert_equal 101, uidplus.source_uids.size
283+
parser.config.parser_max_deprecated_uidplus_data_size = 100
284+
assert_raise_with_message Net::IMAP::ResponseParseError, /uid-set is too large/ do
285+
parser.parse(
286+
"A004 OK [copyUID 1 100:200 1:101] Done\r\n"
287+
)
288+
end
267289
response = parser.parse("A004 OK [copyUID 1 101:200 1:100] Done\r\n")
268290
uidplus = response.data.code.data
269291
assert_instance_of Net::IMAP::UIDPlusData, uidplus
@@ -274,6 +296,7 @@ def test_fetch_binary_and_binary_size
274296
test "COPYUID with parser_use_deprecated_uidplus_data = false" do
275297
parser = Net::IMAP::ResponseParser.new(config: {
276298
parser_use_deprecated_uidplus_data: false,
299+
parser_max_deprecated_uidplus_data_size: 10_000_000,
277300
})
278301
response = parser.parse("A004 OK [COPYUID 1 101 1] Done\r\n")
279302
assert_instance_of Net::IMAP::CopyUIDData, response.data.code.data

0 commit comments

Comments
 (0)