Skip to content

Commit f79d35b

Browse files
authored
🔀 Merge pull request #665 from ruby/backport/v0.5/STARTTLS-stripping
🔒 Fix STARTTLS stripping vulnerability (backports #664)
2 parents 6bf02ae + b3ad198 commit f79d35b

2 files changed

Lines changed: 61 additions & 0 deletions

File tree

lib/net/imap.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1412,9 +1412,11 @@ def logout!
14121412
#
14131413
def starttls(**options)
14141414
@ssl_ctx_params, @ssl_ctx = build_ssl_ctx(options)
1415+
handled = false
14151416
error = nil
14161417
ok = send_command("STARTTLS") do |resp|
14171418
if resp.kind_of?(TaggedResponse) && resp.name == "OK"
1419+
handled = true
14181420
clear_cached_capabilities
14191421
clear_responses
14201422
start_tls_session
@@ -1426,6 +1428,13 @@ def starttls(**options)
14261428
disconnect
14271429
raise error
14281430
end
1431+
unless handled
1432+
disconnect
1433+
raise InvalidResponseError,
1434+
"STARTTLS handler was bypassed, although server responded %p" % [
1435+
ok.raw_data.chomp
1436+
]
1437+
end
14291438
ok
14301439
end
14311440

@@ -3132,6 +3141,7 @@ def idle(timeout = nil, &response_handler)
31323141

31333142
synchronize do
31343143
tag = Thread.current[:net_imap_tag] = generate_tag
3144+
guard_against_tagged_response_skipping_handler!(tag, "IDLE")
31353145
put_string("#{tag} IDLE#{CRLF}")
31363146

31373147
begin
@@ -3596,6 +3606,7 @@ def send_command(cmd, *args, &block)
35963606
put_string(" ")
35973607
send_data(i, tag)
35983608
end
3609+
guard_against_tagged_response_skipping_handler!(tag, cmd)
35993610
put_string(CRLF)
36003611
if cmd == "LOGOUT"
36013612
@logout_command_tag = tag
@@ -3611,6 +3622,19 @@ def send_command(cmd, *args, &block)
36113622
end
36123623
end
36133624
end
3625+
rescue InvalidResponseError
3626+
disconnect
3627+
raise
3628+
end
3629+
3630+
def guard_against_tagged_response_skipping_handler!(tag, cmd)
3631+
return unless (resp = @tagged_responses[tag])&.name&.upcase == "OK"
3632+
raise InvalidResponseError, format(
3633+
"Received tagged 'OK' to incomplete %s command (tag=%s). " \
3634+
"This could indicate a malicious server, a man-in-the-middle, or " \
3635+
"client-side command injection. Disconnecting.",
3636+
cmd, tag
3637+
)
36143638
end
36153639

36163640
def generate_tag

test/net/imap/test_imap.rb

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,43 @@ def test_starttls_stripping
153153
assert_equal(CA_FILE, imap.ssl_ctx.ca_file)
154154
assert_equal(OpenSSL::SSL::VERIFY_PEER, imap.ssl_ctx.verify_mode)
155155
end
156+
157+
def test_starttls_stripping_ok_sent_before_response
158+
# to coordinate between threads (better than sleep)
159+
server_to_client, client_to_server = Queue.new, Queue.new
160+
imap = nil
161+
server = create_tcp_server
162+
port = server.addr[1]
163+
start_server do
164+
sock = server.accept
165+
begin
166+
sock.print("* OK test server\r\n")
167+
assert_equal :send_malicious_response, client_to_server.pop
168+
sock.print("RUBY0001 OK hahaha, fooled you!\r\n")
169+
server_to_client << :malicious_response_sent
170+
sock.gets
171+
ensure
172+
sock.close
173+
server.close
174+
end
175+
end
176+
begin
177+
imap = Net::IMAP.new("localhost", :port => port)
178+
client_to_server << :send_malicious_response
179+
assert_equal :malicious_response_sent, server_to_client.pop
180+
sleep 0.010 # to be sure the network buffers have flushed, etc
181+
assert_raise(Net::IMAP::InvalidResponseError) do
182+
imap.starttls(:ca_file => CA_FILE)
183+
end
184+
assert imap.disconnected?
185+
ensure
186+
imap.disconnect if imap && !imap.disconnected?
187+
end
188+
assert_equal false, imap.tls_verified?
189+
assert_equal({ca_file: CA_FILE}, imap.ssl_ctx_params)
190+
assert_equal(CA_FILE, imap.ssl_ctx.ca_file)
191+
assert_equal(OpenSSL::SSL::VERIFY_PEER, imap.ssl_ctx.verify_mode)
192+
end
156193
end
157194

158195
def start_server

0 commit comments

Comments
 (0)