Skip to content

Commit aefbd2f

Browse files
committed
🥅 Guard against tagged responses sent too soon
The premature tagged response guard that was added for #644 only checked for premature `OK`, not for premature `BAD` or `NO`. When those are detected prior to the tag being sent, this treats those cases as the same sort of error. Note that this explicitly raises a "closed stream" IOError when disconnected. This is the error that would be raised anyway, if `send_command` were allowed to write to the connection. But, if we can test that the socket is already closed, we can raise the error directly. There's no need to attempt to format and send data.
1 parent f5b0ea0 commit aefbd2f

2 files changed

Lines changed: 30 additions & 13 deletions

File tree

lib/net/imap.rb

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3201,8 +3201,8 @@ def idle(timeout = nil, &response_handler)
32013201
response = nil
32023202

32033203
synchronize do
3204-
tag = Thread.current[:net_imap_tag] = generate_tag
3205-
command = Command[tag:, name: "IDLE"]
3204+
command = start_command(name: "IDLE")
3205+
tag = Thread.current[:net_imap_tag] = command.tag # TODO: remove this
32063206
put_string("#{tag} IDLE#{CRLF}")
32073207
finish_sending_command(command)
32083208

@@ -3665,8 +3665,8 @@ def send_command(cmd, *args, &block)
36653665
args.each do |i|
36663666
validate_data(i)
36673667
end
3668-
tag = generate_tag
3669-
command = Command[tag:, name: cmd]
3668+
command = start_command(name: cmd)
3669+
tag = command.tag
36703670
put_string(tag + " " + cmd)
36713671
args.each do |i|
36723672
put_string(" ")
@@ -3687,6 +3687,16 @@ def send_command(cmd, *args, &block)
36873687
raise
36883688
end
36893689

3690+
def start_command(**)
3691+
raise IOError, "closed stream" if disconnected?
3692+
tag = generate_tag
3693+
command = Command.new(**, tag:)
3694+
if (response = @tagged_responses[command.tag])
3695+
raise InvalidTaggedResponseError.new(:unstarted, command:, response:)
3696+
end
3697+
command
3698+
end
3699+
36903700
# NOTE: This must be synchronized with sending the command's final CRLF and
36913701
# adding any command-related response handlers.
36923702
def finish_sending_command(command)

test/net/imap/test_imap.rb

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -210,13 +210,16 @@ def test_starttls_stripping_ok_sent_before_response
210210
end
211211
end
212212

213-
# Similar to STARTTLS stripping test, but checks other commands too
213+
# Similar to STARTTLS stripping test, but checks other commands and statuses
214214
data(
215-
"IDLE" => ->imap do imap.idle(1) do end end,
216-
"NOOP" => ->imap do imap.noop end,
217-
"SELECT" => ->imap do imap.select("inbox") end,
215+
"OK for IDLE" => ["OK", ->imap do imap.idle(1) do end end],
216+
"OK for NOOP" => ["OK", ->imap do imap.noop end],
217+
"NO for IDLE" => ["NO", ->imap do imap.idle(1) do end end],
218+
"NO for EXAMINE" => ["NO", ->imap do imap.examine("inbox") end],
219+
"BAD for IDLE" => ["BAD", ->imap do imap.idle(1) do end end],
220+
"BAD for SELECT" => ["BAD", ->imap do imap.select("inbox") end],
218221
)
219-
test "premature tagged OK response" do |cmd|
222+
test "premature tagged response" do |(status, cmd)|
220223
timeout = 5
221224
timeout *= EnvUtil.timeout_scale || 1 if defined?(EnvUtil.timeout_scale)
222225
Timeout.timeout(timeout) do
@@ -230,9 +233,9 @@ def test_starttls_stripping_ok_sent_before_response
230233
begin
231234
sock.print("* OK test server\r\n")
232235
assert_equal :send_malicious_responses, client_to_server.pop
233-
sock.print("RUBY0001 OK invalid\r\n")
234-
sock.print("RUBY0002 OK false\r\n")
235-
sock.print("RUBY0003 OK tricky\r\n")
236+
sock.print("RUBY0001 #{status} invalid\r\n")
237+
sock.print("RUBY0002 #{status} false\r\n")
238+
sock.print("RUBY0003 #{status} tricky\r\n")
236239
server_to_client << :sent_malicious_responses
237240
sock.gets
238241
ensure
@@ -250,7 +253,11 @@ def test_starttls_stripping_ok_sent_before_response
250253
assert_equal :sent_malicious_responses, server_to_client.pop
251254
assert_equal [1, 2, 3], 3.times.map { rcvr_to_client.pop }
252255
# should respond this way for _any_ command
253-
assert_raise(Net::IMAP::InvalidTaggedResponseError) do cmd.(imap) end
256+
assert_raise_with_message(
257+
Net::IMAP::InvalidTaggedResponseError, / unstarted .*tag=RUBY0001/
258+
) do
259+
cmd.(imap)
260+
end
254261
assert imap.disconnected?
255262
assert_stream_closed_error do cmd.(imap) end
256263
assert_stream_closed_error do cmd.(imap) end

0 commit comments

Comments
 (0)