Skip to content

Commit 24a4e77

Browse files
authored
🔀 Merge pull request #666 from ruby/backport/v0.4/STARTTLS-stripping
🔒 Fix STARTTLS stripping vulnerability (backports #664)
2 parents 63f53ff + 038ae35 commit 24a4e77

2 files changed

Lines changed: 58 additions & 0 deletions

File tree

lib/net/imap.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1312,9 +1312,11 @@ def logout!
13121312
#
13131313
def starttls(**options)
13141314
@ssl_ctx_params, @ssl_ctx = build_ssl_ctx(options)
1315+
handled = false
13151316
error = nil
13161317
ok = send_command("STARTTLS") do |resp|
13171318
if resp.kind_of?(TaggedResponse) && resp.name == "OK"
1319+
handled = true
13181320
clear_cached_capabilities
13191321
clear_responses
13201322
start_tls_session
@@ -1326,6 +1328,13 @@ def starttls(**options)
13261328
disconnect
13271329
raise error
13281330
end
1331+
unless handled
1332+
disconnect
1333+
raise InvalidResponseError,
1334+
"STARTTLS handler was bypassed, although server responded %p" % [
1335+
ok.raw_data.chomp
1336+
]
1337+
end
13291338
ok
13301339
end
13311340

@@ -3052,6 +3061,7 @@ def send_command(cmd, *args, &block)
30523061
put_string(" ")
30533062
send_data(i, tag)
30543063
end
3064+
guard_against_tagged_response_skipping_handler!(tag)
30553065
put_string(CRLF)
30563066
if cmd == "LOGOUT"
30573067
@logout_command_tag = tag
@@ -3067,6 +3077,17 @@ def send_command(cmd, *args, &block)
30673077
end
30683078
end
30693079
end
3080+
rescue InvalidResponseError
3081+
disconnect
3082+
raise
3083+
end
3084+
3085+
def guard_against_tagged_response_skipping_handler!(tag)
3086+
return unless (resp = @tagged_responses[tag])&.name&.upcase == "OK"
3087+
raise(InvalidResponseError,
3088+
"Server sent tagged 'OK' before command was finished: %p. " \
3089+
"This could indicate a malicious server or client-side " \
3090+
"command injection. Disconnecting." % [resp.raw_data.chomp])
30703091
end
30713092

30723093
def generate_tag

test/net/imap/test_imap.rb

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

163200
def start_server

0 commit comments

Comments
 (0)