Skip to content

Commit a445c8c

Browse files
committed
✨ Enable parenthesized lists in search criteria
This affects search, uid_search, sort, uid_sort, thread, and uid_thread. Prior to this, sending a parenthesized list in the search criteria for any of these commands required the use of strings, which are converted to RawData, which has security implications with untrusted inputs. With this change, arrays will only be converted into SequenceSet when _every_ element in the array is a valid SequenceSet input. Otherwise, the array will be left alone, which allows us to send parenthesized lists without using strings and RawData. For example, some search criteria this change enables: * `["not", %w[flagged unread]]` converts to `not (flagged unread)`. * `["return", ["partial", 1..50]]` converts to `return (partial 1:50)`.
1 parent 14e2c6c commit a445c8c

File tree

3 files changed

+49
-41
lines changed

3 files changed

+49
-41
lines changed

lib/net/imap.rb

Lines changed: 13 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1939,8 +1939,9 @@ def uid_expunge(uid_set)
19391939
# provided as an array or a string.
19401940
#
19411941
# * When +criteria+ is an array, any array members will be converted into
1942-
# a SequenceSet. _NOTE:_ this means that a parenthesized list cannot be
1943-
# sent when +criteria+ is an array.
1942+
# SequenceSet if all of their values are integers, ranges of integers,
1943+
# <tt>"*"</tt>, +:*+, or a deeply nested array of these types. All other
1944+
# array members will be converted to a parenthesized list.
19441945
#
19451946
# * When +criteria+ is a string, it will be sent directly to the server
19461947
# <em>without any validation</em>. *WARNING:* This is vulnerable to
@@ -1956,8 +1957,8 @@ def uid_expunge(uid_set)
19561957
# Do not use the +charset+ argument when it is embedded in +criteria+.
19571958
# For example:
19581959
#
1959-
# imap.search("CHARSET UTF-8 OR UNSEEN FLAGGED")
1960-
# imap.search(%w[CHARSET UTF-8 OR UNSEEN FLAGGED])
1960+
# imap.search("CHARSET UTF-8 (OR UNSEEN FLAGGED)")
1961+
# imap.search(["CHARSET", "UTF-8", %w(OR UNSEEN FLAGGED)])
19611962
#
19621963
# Related: #uid_search
19631964
#
@@ -2963,17 +2964,10 @@ def enforce_logindisabled?
29632964
end
29642965

29652966
def search_internal(cmd, keys, charset)
2966-
if keys.instance_of?(String)
2967-
keys = [RawData.new(keys)]
2968-
else
2969-
normalize_searching_criteria(keys)
2970-
end
2967+
keys = normalize_searching_criteria(keys)
2968+
args = charset ? ["CHARSET", charset, *keys] : keys
29712969
synchronize do
2972-
if charset
2973-
send_command(cmd, "CHARSET", charset, *keys)
2974-
else
2975-
send_command(cmd, *keys)
2976-
end
2970+
send_command(cmd, *args)
29772971
clear_responses("SEARCH").last || []
29782972
end
29792973
end
@@ -3020,38 +3014,24 @@ def copy_internal(cmd, set, mailbox)
30203014
end
30213015

30223016
def sort_internal(cmd, sort_keys, search_keys, charset)
3023-
if search_keys.instance_of?(String)
3024-
search_keys = [RawData.new(search_keys)]
3025-
else
3026-
normalize_searching_criteria(search_keys)
3027-
end
3017+
search_keys = normalize_searching_criteria(search_keys)
30283018
synchronize do
30293019
send_command(cmd, sort_keys, charset, *search_keys)
30303020
clear_responses("SORT").last || []
30313021
end
30323022
end
30333023

30343024
def thread_internal(cmd, algorithm, search_keys, charset)
3035-
if search_keys.instance_of?(String)
3036-
search_keys = [RawData.new(search_keys)]
3037-
else
3038-
normalize_searching_criteria(search_keys)
3039-
end
3025+
search_keys = normalize_searching_criteria(search_keys)
30403026
synchronize do
30413027
send_command(cmd, algorithm, charset, *search_keys)
30423028
clear_responses("THREAD").last || []
30433029
end
30443030
end
30453031

3046-
def normalize_searching_criteria(keys)
3047-
keys.collect! do |i|
3048-
case i
3049-
when -1, Range, Array
3050-
SequenceSet.new(i)
3051-
else
3052-
i
3053-
end
3054-
end
3032+
def normalize_searching_criteria(criteria)
3033+
return RawData.new(criteria) if criteria.is_a?(String)
3034+
criteria.map {|i| SequenceSet::Coercible[i] ? SequenceSet[i] : i }
30553035
end
30563036

30573037
def build_ssl_ctx(ssl)

lib/net/imap/sequence_set.rb

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -276,16 +276,29 @@ class SequenceSet
276276
# The largest possible non-zero unsigned 32-bit integer
277277
UINT32_MAX = 2**32 - 1
278278

279+
REGEXP = ResponseParser::Patterns::SEQUENCE_SET_STR
280+
private_constant :REGEXP
281+
279282
# represents "*" internally, to simplify sorting (etc)
280283
STAR_INT = UINT32_MAX + 1
281284
private_constant :STAR_INT
282285

283286
# valid inputs for "*"
284287
STARS = [:*, ?*, -1].freeze
285-
private_constant :STAR_INT, :STARS
288+
private_constant :STARS
286289

287-
COERCIBLE = ->{ _1.respond_to? :to_sequence_set }
288-
private_constant :COERCIBLE
290+
# Matches objects which should be implicitly converted into SequenceSet
291+
# objects. Note that the inputs are not validated, and some valid inputs
292+
# (Enumerable other than Array or Set) will be rejected.
293+
Coercible = ->(obj) do
294+
case obj
295+
when SequenceSet then true
296+
when Integer, Range, *STARS then true
297+
when String then REGEXP.match?(obj.b)
298+
when Array, Set then obj.all?(Coercible) && !obj.empty?
299+
else obj.respond_to?(:to_sequence_set)
300+
end
301+
end
289302

290303
class << self
291304

test/net/imap/test_imap.rb

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1206,16 +1206,31 @@ def test_unselect
12061206
end
12071207

12081208
server.on "SEARCH", &search_resp
1209-
assert_equal search_result, imap.search(["subject", "hello",
1210-
[1..5, 8, 10..-1]])
1209+
server.on "UID SEARCH", &search_resp
1210+
1211+
assert_equal search_result, imap.search(
1212+
["subject", "hello", [1..5, 8, 10..-1]]
1213+
)
12111214
cmd = server.commands.pop
12121215
assert_equal ["SEARCH", "subject hello 1:5,8,10:*"], [cmd.name, cmd.args]
12131216

1214-
server.on "UID SEARCH", &search_resp
1215-
assert_equal search_result, imap.uid_search(["subject", "hello",
1216-
[1..22, 30..-1]])
1217+
assert_equal search_result, imap.uid_search(
1218+
["subject", "hello", [1..22, 30..-1]]
1219+
)
12171220
cmd = server.commands.pop
12181221
assert_equal ["UID SEARCH", "subject hello 1:22,30:*"], [cmd.name, cmd.args]
1222+
1223+
assert_equal search_result, imap.search(
1224+
"RETURN (COUNT) NOT (FLAGGED (OR SEEN ANSWERED))"
1225+
)
1226+
cmd = server.commands.pop
1227+
assert_equal "RETURN (COUNT) NOT (FLAGGED (OR SEEN ANSWERED))", cmd.args
1228+
1229+
assert_equal search_result, imap.search([
1230+
"RETURN", %w(MIN MAX COUNT), "NOT", ["FLAGGED", %w(OR SEEN ANSWERED)]
1231+
])
1232+
cmd = server.commands.pop
1233+
assert_equal "RETURN (MIN MAX COUNT) NOT (FLAGGED (OR SEEN ANSWERED))", cmd.args
12191234
end
12201235
end
12211236

0 commit comments

Comments
 (0)