Skip to content

Commit 5431e16

Browse files
committed
✨ Make max_response_size configurable [🚧 partial]
Note that this cherry-picked commit is missing key paits that are incompatible with net-imap before 0.4. I'm keeping the conflict resolution here, and the updates for net-imap 0.3 in the next commit. ------ Though it would be useful to also have limits based on response type and what commands are currently running, that's out of scope for now. _Please note:_ this only limits the size per response. It does _not_ limit how many unhandled responses may be stored on the responses hash.
1 parent 20c16a2 commit 5431e16

File tree

4 files changed

+86
-11
lines changed

4 files changed

+86
-11
lines changed

lib/net/imap.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,10 @@ module Net
133133
#
134134
# Use paginated or limited versions of commands whenever possible.
135135
#
136+
# Use Config#max_response_size to impose a limit on incoming server responses
137+
# as they are being read. <em>This is especially important for untrusted
138+
# servers.</em>
139+
#
136140
# Use #add_response_handler to handle responses after each one is received.
137141
# Use the +response_handlers+ argument to ::new to assign response handlers
138142
# before the receiver thread is started.
@@ -313,6 +317,17 @@ class << self
313317
alias default_ssl_port default_tls_port
314318
end
315319

320+
##
321+
# :attr_accessor: max_response_size
322+
#
323+
# The maximum allowed server response size, in bytes.
324+
# Delegates to {config.max_response_size}[rdoc-ref:Config#max_response_size].
325+
326+
# :stopdoc:
327+
def max_response_size; config.max_response_size end
328+
def max_response_size=(val) config.max_response_size = val end
329+
# :startdoc:
330+
316331
# Disconnects from the server.
317332
def disconnect
318333
return if disconnected?

lib/net/imap/response_reader.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def read_limit(limit = nil)
5252
[limit, max_response_remaining!].compact.min
5353
end
5454

55-
def max_response_size = 512 << 20 # TODO: Config#max_response_size
55+
def max_response_size = client.max_response_size
5656
def max_response_remaining = max_response_size &.- bytes_read
5757
def response_too_large? = max_response_size &.< min_response_size
5858
def min_response_size = bytes_read + min_response_remaining
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# frozen_string_literal: true
2+
3+
require "net/imap"
4+
require "test/unit"
5+
require_relative "fake_server"
6+
7+
class IMAPMaxResponseSizeTest < Test::Unit::TestCase
8+
include Net::IMAP::FakeServer::TestHelper
9+
10+
def setup
11+
Net::IMAP.config.reset
12+
@do_not_reverse_lookup = Socket.do_not_reverse_lookup
13+
Socket.do_not_reverse_lookup = true
14+
@threads = []
15+
end
16+
17+
def teardown
18+
if !@threads.empty?
19+
assert_join_threads(@threads)
20+
end
21+
ensure
22+
Socket.do_not_reverse_lookup = @do_not_reverse_lookup
23+
end
24+
25+
test "#max_response_size reading literals" do
26+
with_fake_server(preauth: true) do |server, imap|
27+
imap.max_response_size = 12_345 + 30
28+
server.on("NOOP") do |resp|
29+
resp.untagged("1 FETCH (BODY[] {12345}\r\n" + "a" * 12_345 + ")")
30+
resp.done_ok
31+
end
32+
imap.noop
33+
assert_equal "a" * 12_345, imap.responses("FETCH").first.message
34+
end
35+
end
36+
37+
test "#max_response_size closes connection for too long line" do
38+
Net::IMAP.config.max_response_size = 10
39+
run_fake_server_in_thread(preauth: false, ignore_io_error: true) do |server|
40+
assert_raise_with_message(
41+
Net::IMAP::ResponseTooLargeError, /exceeds max_response_size .*\b10B\b/
42+
) do
43+
with_client("localhost", port: server.port) do
44+
fail "should not get here (greeting longer than max_response_size)"
45+
end
46+
end
47+
end
48+
end
49+
50+
test "#max_response_size closes connection for too long literal" do
51+
Net::IMAP.config.max_response_size = 1<<20
52+
with_fake_server(preauth: false, ignore_io_error: true) do |server, client|
53+
client.max_response_size = 50
54+
server.on("NOOP") do |resp|
55+
resp.untagged("1 FETCH (BODY[] {1000}\r\n" + "a" * 1000 + ")")
56+
end
57+
assert_raise_with_message(
58+
Net::IMAP::ResponseTooLargeError,
59+
/\d+B read \+ 1000B literal.* exceeds max_response_size .*\b50B\b/
60+
) do
61+
client.noop
62+
fail "should not get here (FETCH literal longer than max_response_size)"
63+
end
64+
end
65+
end
66+
67+
end

test/net/imap/test_response_reader.rb

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
class ResponseReaderTest < Test::Unit::TestCase
88
class FakeClient
9+
def max_response_size = config.max_response_size
910
end
1011

1112
def literal(str) "{#{str.bytesize}}\r\n#{str}" end
@@ -44,22 +45,14 @@ def literal(str) "{#{str.bytesize}}\r\n#{str}" end
4445
assert_equal "", rcvr.read_response_buffer.to_str
4546
end
4647

47-
class LimitedResponseReader < Net::IMAP::ResponseReader
48-
attr_reader :max_response_size
49-
def initialize(*args, max_response_size:)
50-
super(*args)
51-
@max_response_size = max_response_size
52-
end
53-
end
54-
5548
test "#read_response_buffer with max_response_size" do
5649
client = FakeClient.new
57-
max_response_size = 10
50+
client.config.max_response_size = 10
5851
under = "+ 3456\r\n"
5952
exact = "+ 345678\r\n"
6053
over = "+ 3456789\r\n"
6154
io = StringIO.new([under, exact, over].join)
62-
rcvr = LimitedResponseReader.new(client, io, max_response_size:)
55+
rcvr = Net::IMAP::ResponseReader.new(client, io)
6356
assert_equal under, rcvr.read_response_buffer.to_str
6457
assert_equal exact, rcvr.read_response_buffer.to_str
6558
assert_raise Net::IMAP::ResponseTooLargeError do

0 commit comments

Comments
 (0)