Skip to content

Commit

Permalink
Merge commit from fork
Browse files Browse the repository at this point in the history
🔒 Prevent runaway memory use when parsing `uid-set` (v0.4 backport)
  • Loading branch information
nevans authored Feb 7, 2025
2 parents e4d57b1 + abff00f commit c8c5a64
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 4 deletions.
42 changes: 41 additions & 1 deletion 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 @@ -282,12 +288,41 @@ def self.[](config)
# [+true+ <em>(original default)</em>]
# ResponseParser only uses UIDPlusData.
#
# [+:up_to_max_size+ <em>(default since +v0.5.6+)</em>]
# ResponseParser uses UIDPlusData when the +uid-set+ size is below
# parser_max_deprecated_uidplus_data_size. Above that size,
# ResponseParser uses AppendUIDData or CopyUIDData.
#
# [+false+ <em>(planned default for +v0.6+)</em>]
# ResponseParser _only_ uses AppendUIDData and CopyUIDData.
attr_accessor :parser_use_deprecated_uidplus_data, type: [
true, false
true, :up_to_max_size, 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 +403,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 +413,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 +422,8 @@ def defaults_hash

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

version_defaults[:default] = Config[0.4]
Expand All @@ -394,6 +433,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
13 changes: 10 additions & 3 deletions lib/net/imap/response_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1889,9 +1889,16 @@ def CopyUID(...) DeprecatedUIDPlus(...) || CopyUIDData.new(...) end
# TODO: remove this code in the v0.6.0 release
def DeprecatedUIDPlus(validity, src_uids = nil, dst_uids)
return unless config.parser_use_deprecated_uidplus_data
src_uids &&= src_uids.each_ordered_number.to_a
dst_uids = dst_uids.each_ordered_number.to_a
UIDPlusData.new(validity, src_uids, dst_uids)
compact_uid_sets = [src_uids, dst_uids].compact
count = compact_uid_sets.map { _1.count_with_duplicates }.max
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
UIDPlusData.new(validity, src_uids, dst_uids)
elsif config.parser_use_deprecated_uidplus_data != :up_to_max_size
parse_error("uid-set is too large: %d > %d", count, max)
end
end

ADDRESS_REGEXP = /\G
Expand Down
57 changes: 57 additions & 0 deletions test/net/imap/test_imap_response_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -205,16 +205,43 @@ 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
assert_equal 100, uidplus.assigned_uids.size
end

test "APPENDUID with parser_use_deprecated_uidplus_data = :up_to_max_size" do
parser = Net::IMAP::ResponseParser.new(config: {
parser_use_deprecated_uidplus_data: :up_to_max_size,
parser_max_deprecated_uidplus_data_size: 100
})
response = parser.parse("A004 OK [APPENDUID 1 101:200] Done\r\n")
assert_instance_of Net::IMAP::UIDPlusData, response.data.code.data
response = parser.parse("A004 OK [APPENDUID 1 100:200] Done\r\n")
assert_instance_of Net::IMAP::AppendUIDData, response.data.code.data
end

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 @@ -253,17 +280,47 @@ 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
assert_equal 100, uidplus.assigned_uids.size
assert_equal 100, uidplus.source_uids.size
end

test "COPYUID with parser_use_deprecated_uidplus_data = :up_to_max_size" do
parser = Net::IMAP::ResponseParser.new(config: {
parser_use_deprecated_uidplus_data: :up_to_max_size,
parser_max_deprecated_uidplus_data_size: 100
})
response = parser.parse("A004 OK [COPYUID 1 101:200 1:100] Done\r\n")
copyuid = response.data.code.data
assert_instance_of Net::IMAP::UIDPlusData, copyuid
response = parser.parse("A004 OK [COPYUID 1 100:200 1:101] Done\r\n")
copyuid = response.data.code.data
assert_instance_of Net::IMAP::CopyUIDData, copyuid
end

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 c8c5a64

Please sign in to comment.